トラック
/
Crystal
Crystal
/
演習
/
パスワードロック
パスワードロック

パスワードロック

学習演習

はじめに

Crystalでは、変数が複数の型を持つことができます。これをユニオン型と呼びます。Crystalでは、コンパイラーがユニオン型を推論するのが一般的です。

Note

ユニオン型は、複数の型からできていても、実行時には1つの型です。つまり、StringとInt32からなるユニオン型が、同時にその両方になることはありません。その代わり、StringかInt32のどちらかになります。

ユニオン型は、型をパイプ(|)で区切って宣言します。多くの場合、括弧(())で囲みますが、必須ではありません。もっともよく見るユニオン型は、Tを型とすると(T | Nil)で、これはNilを返す可能性があるメソッドに現れます。これは、その変数がnil可能であるとも言われます。

foo : String = "Hello"
foo = nil # Error: type must be String, not (String | Nil)

foo : (String | Nil) = "Hello"
foo = nil

型は2つに限らず、いくつでも組み合わせられます。

foo : (String | Int32 | Nil | Float64) = "Hello"
foo = 1
foo = nil
foo = 1.0

typeofとObject#classの違い

変数の型を調べる方法は2つあります。typeofを使うか、Object#classを使うかです。違いは、typeofがコンパイル時に変数の型を返すのに対し、Object#classは実行時に型を返すことです。つまり、たとえば変数がユニオン型かどうかを調べたいとき、Object#classではそれがわかりません。実行時に返るのは1つの型だけだからです。

foo = 0 == 0 ? "a" : 1
typeof(foo) # => (String | Int32)
foo.class # => String

ユニオン型の操作

ユニオン型は実行時には1つの型なので、通常の操作はすべて使えます。ただし、コンパイル時にはコンパイラーがどの型なのかを知っている必要があります。そのため、型ごとの操作を使いたいときは、その型のどれか1つにしかなり得ないようにコードを書く必要があります。

foo : (String | Int32) = "Hello"
foo.downcase # Error: undefined method 'downcase' for (String | Int32)

Crystalには、ユニオン型のための特別なメソッドがあります。is_a?メソッドです。これは型を引数に取り、真偽値を返します。nil?メソッドはis_a?(Nil)の省略形です。is_a?メソッドを制御式の中で使うと、コンパイラーにその型であることを伝えられ、その型であることが保証されます。また、elseの分岐では、その型ではないことが保証されます。

foo : (String | Int32) = "Hello"
if foo.is_a?(String)
  typeof(foo)  # => String
  foo.downcase # => "hello"
end

このis_a?は、引数に1つの型だけでなくユニオン型を取ることもできます。&&と組み合わせて複数の型を指定することもできます。

Note

is_a?メソッドを制御式と組み合わせて使う場合、そのレシーバーをインスタンス変数やクラス変数にすることはできません。その場合は、まずローカル変数に代入する必要があります。

as

ユニオン型を1つの型にする方法の1つは、特定の型のときだけその分岐に入るようにすることです。もう1つの方法は、asメソッドを使うことです。これは実行時にチェックすることで、ユニオン型を1つの型にします。型が期待したものでなければ、例外が発生します。

foo : String | Int32 = "Hello"
foo.as(String).downcase # => "hello"

foo.as(Int32) # Error: can't cast String to Int32
Caution

この方法は、型が期待したものであると確信しているとき、またはそうでないときに例外を発生させたいときだけのものです。

不適切な使い方をすると、予期しない動作につながることがあります。

as?

as?はasとよく似た働きをしますが、型が期待したものでないときに例外を発生させる代わりにnilを返します。つまり、期待した型とNilのユニオン型を返します。

foo : (String | Int32) = "Hello"
foo.as?(String).downcase # => "hello"

foo.as?(Int32) # => nil

nil可能の省略形

nil可能とは、変数が何らかの型かNilのどちらかになり得るという意味です。これは(T | Nil)と書けます。ただし、nil可能な型は比較的よく登場するので、省略形が用意されています。T?です。

# This:
foo : (String | Nil) = "Hello"
foo = nil

# Is the same as:
foo : String? = "Hello"
foo = nil

説明

勤めている会社が、まもなくSmarty 5という新しいスマートフォンを発売しようとしています。新しいカメラシステム、新しいデザイン、そしてsmartyOSという新しいオペレーティングシステムで動く、革新的な新しいプロセッサーを搭載しています。

新しいプロセッサーは、これまで以上に安全なパスワードを扱える性能を備えています。会社はこれを活用することにしました。そして、スマートフォン用のパスワードロックシステムの実装を依頼してきました。これは、ユーザーがパスワードを設定し、そのあと、入力されたパスワードが正しいかどうかを確認できるようにするものです。

Caution

この演習で扱うパスワードシステムは安全ではなく、教育目的でのみ使われます。実際のアプリケーションで使うべきではありません。

1. パスワードを設定する

このスマートフォンでは、ユーザーがいくつもの異なる種類のパスワードを使えます。単純な数字のパスワードから、より安全な英数字のパスワード、さらには指紋を記憶するパスワードまであります。

これらのパスワードはそれぞれ異なる型を使います。数字のパスワードはInt32、英数字のパスワードはString、指紋のパスワードはFloat64を使います。

PasswordLockクラスを実装してください。イニシャライザーは3種類のパスワードのいずれかを引数に取り、それを@passwordというインスタンス変数に保存します。

password_lock = PasswordLock.new(1234)
# => #<PasswordLock:0x7f8e1b8c0b80 @password=1234>

2. パスワードを暗号化する

会社は、パスワードが平文で保存されないよう暗号化することにしました。そして、それを暗号化するメソッドの実装を依頼してきました。

パスワードの種類ごとに、暗号化の方法が異なります。

  • Int32のパスワード:パスワードを2で割り、最も近い整数に丸めます。
  • Stringのパスワード:パスワードを逆にします。
  • Float64のパスワード:パスワードを4倍します。

encryptというインスタンスメソッドを実装してください。このメソッドは引数を取らず、@passwordインスタンス変数を暗号化された状態に変更します。

password_lock = PasswordLock.new(1234)
# => #<PasswordLock:0x7f8e1b8c0b80 @password=1234>

password_lock.encrypt
# => #<PasswordLock:0x7f8e1b8c0b80 @password=617>

3. パスワードが正しいかどうかを確認する

会社は、入力されたパスワードが正しいかどうかを確認するメソッドの実装も依頼してきました。

パスワードが正しくなければnilを、正しければ"Unlocked"を返すメソッドにしたいのです。

unlock?というインスタンスメソッドを実装してください。確認するパスワードを引数に取ります。 このメソッドは、パスワードが正しくなければnilを、正しければ"Unlocked"を返します。

password_lock = PasswordLock.new(1234)
password_lock.encrypt
password_lock.unlock?(1234)
# => "Unlocked"
GitHubで編集する リンクは新しいウィンドウまたはタブで開きます
Crystal Exercism

パスワードロックを始める準備はできましたか?

Exercismに登録すれば、26個のコンセプト142個の演習、そして本物の人間によるメンタリングとともに、Crystalを学んでマスターできます。すべて無料です。