基本型
この章では、Integers、Floats、Booleans、Atoms、Strings、Lists、Tuples など Elixir の基本的なデータ型について学んでいきます。
iex> 1 # integer
iex> 0x1F # integer
iex> 1.0 # float
iex> true # boolean
iex> :atom # atom / symbol
iex> "elixir" # string
iex> [1, 2, 3] # list
iex> {1, 2, 3} # tuple
四則演算
iex
コマンドを起動したら、以下のように入力してみてください。
iex> 1 + 2
3
iex> 5 * 5
25
iex> 10 / 2
5.0
10 / 2
の結果が Integer の 5
ではなく 5.0
という Float として得られたはずです。Elixir において /
は常に Float を返します。除算や剰余で整数を得たい場合には、div
や rem
関数を使用できます。
iex> div(10, 2)
5
iex> div 10, 2
5
iex> rem 10, 3
1
Elixir は関数を呼び出す際の括弧を省略できます。これにより、宣言と制御構造での文法的な見晴らしをクリアにします。
2 進法、8 進法、および 16 進法もサポートしています。
iex> 0b1010
10
iex> 0o777
511
iex> 0x1F
31
浮動小数点数は仮数と小数点に続いて小数を必要とし、指数表記の e
も使用できます。
iex> 1.0
1.0
iex> 1.0e-10
1.0e-10
Elixir の Floats は倍精度浮動小数点数です。
round
や trunc
関数では、引数として与えられた Float をもとに最も近い仮数を得られます。
iex> round(3.58)
4
iex> trunc(3.58)
3
関数の確認
Elixir における関数は、その関数名とアリティで成り立ちます。アリティはその関数が受け取る引数の数を示します。これ以降は関数を説明するにあたって、関数名とそのアリティの両方を添えて記述していきます。round/1
は round
という関数とその関数が受け取る引数の数 1
を示します。一方で、例えば round/2
という関数があった場合に、それは関数名として同名ではあるものの、前者とは異なって引数を 2
受け取る別の関数です。
真偽値
Elixir では true
と false
を 真偽値 としています。
iex> true
true
iex> true == false
false
述語的な名前を持った一連の関数 (Predicate functions) を使って、値の型をチェックすることも出来ます。例えば is_boolean/1
関数は、値が真偽値か否かをチェックする為に使用します。
iex> is_boolean(true)
true
iex> is_boolean(1)
false
さらに、is_integer/1
や is_float/1
、is_number/1
なども同様に、それぞれの引数が整数・浮動小数点数・数値であるかをチェックできます。
Note:
h()
でシェルの使い方に関する情報を表示できます。h
ヘルパーは関数に関するドキュメントを参照する際にも使用できます。
アトム
アトムは他のいくつかの言語で言うところのシンボルに相当し、それ自身が定数でもあります。
iex> :hello
:hello
iex> :hello == :world
false
実は true
と false
もアトムです。
iex> true == :true
true
iex> is_atom(false)
true
iex> is_boolean(:false)
true
後で触れますが、Elixir はエイリアスと呼ばれる機能を持っています。エイリアスは大文字から始め、それもまた同時にアトムなのです。
iex> is_atom(Hello)
true
文字列
文字列リテラルはダブルクォーテーションで囲み、UTF-8 でエンコードされます。
iex> "hellö"
"hellö"
Note: Windows をお使いの方は、デフォルトで UTF-8 を使用できない可能性があります。これを変更するには、 IEx を起動する前に、
chcp 65001
を実行してください。
Elixir は文字列内での式展開もサポートしています。
iex> "hellö #{:world}"
"hellö world"
文字列内では改行することができ、エスケープシーケンスも利用できます。
iex> "hello
...> world"
"hello\nworld"
iex> "hello\nworld"
"hello\nworld"
文字列の出力には IO
モジュールの IO.puts/1
関数を使用します。
iex> IO.puts "hello\nworld"
hello
world
:ok
IO.puts/1
関数がアトムの :ok
を返していることにも留意してください。
Elixir における文字列は、内部的にバイト列での表現もなされています。
iex> is_binary("hellö")
true
文字列のバイト数も得られます。
iex> byte_size("hellö")
6
上記の文字列は 5 字ですが、バイト数としては 6 が得られました。 “ö” を UTF-8 で表す為には 2 バイトを要するからです。String.length/1
を使用すると、その文字数に基づいた文字列の長さを得ることもできます。
iex> String.length("hellö")
5
String モジュール には、Unicode に基づいて定義された文字列を操作する為の様々な関数が含まれています。
iex> String.upcase("hellö")
"HELLÖ"
無名関数
無名関数は fn
と end
で囲んだ内側で定義されます。
iex> add = fn a, b -> a + b end
#Function<12.71889879/2 in :erl_eval.expr/5>
iex> add.(1, 2)
3
iex> is_function(add)
true
iex> is_function(add, 2) # add が 2 つの引数を期待する関数であるのかを確かめる
true
iex> is_function(add, 1) # add が 1 つの引数を期待する関数であるのかを確かめる
false
「Elixir における関数が”第一級オブジェクト”である」ということは、関数そのものも Integer や String と同様に他の関数へ引数として渡すことが出来るという意味です。例として、私達は先ほど変数 add
にバインドした関数を、引数が関数であれば true
を返すという is_function/1
関数に渡しました。さらには is_function/2
関数を使用してアリティを確かめることも出来ました。
無名関数の実行には変数と括弧の間にドット (.
) を必要とします。このドットによって、 add
という無名関数の呼び出しと add/2
という関数の呼び出しを区別し、曖昧がないことを保証します。そういった意味で Elixir は無名関数と通常の関数とを明確に区別します。それについて詳しくは 第 8 章 で取り上げます。
無名関数はクロージャであり、関数が定義された際のスコープ内にある変数へはそのままアクセスできます。それでは、先ほど定義した add
を内包する無名関数を新たに定義してみましょう。
iex> double = fn a -> add.(a, a) end
#Function<6.71889879/1 in :erl_eval.expr/5>
iex> double.(2)
4
関数内部に置かれた変数は、その周囲の環境には影響がないことに注意してください。
iex> x = 42
42
iex> (fn -> x = 0 end).()
0
iex> x
42
(連結)リスト
リストを作るには角括弧を使って記述します。リストの要素はどんな型でも構いません。
iex> [1, 2, true, 3]
[1, 2, true, 3]
iex> length [1, 2, 3]
3
++/2
や --/2
を使えば、2つのリストを足したり引いたり出来ます。
iex> [1, 2, 3] ++ [4, 5, 6]
[1, 2, 3, 4, 5, 6]
iex> [1, true, 2, false, 3, true] -- [true, false]
[1, 2, 3, true]
リストへの操作は既にあるリストを改変しません。リストを足したり引いたりすると、新しく別のリストを返します。Elixir のデータ構造は イミュータブル です。イミュータブルであることのメリットとして、明快なコードを書けるということが挙げられます。このおかげで、データが非破壊であるという保証のもとに我々はデータの自由なやり取りを行うことが出来ます。
このチュートリアルを通してリストの先頭と後尾について何度か触れました。先頭はリストの最初の要素であり、後尾はリストの残りの要素です。これらは hd/1
と tl/1
で取り出すことができます。では、リストに要素を加えて先頭や後尾からそれら要素を取り出してみましょう。
iex> list = [1, 2, 3]
iex> hd(list)
1
iex> tl(list)
[2, 3]
空のリストから先頭や後尾の要素を得ようとするとエラーが返ってきます。
iex> hd []
** (ArgumentError) argument error
作成したリストがシングルクォーテーションで要素を返すことがあります。
iex> [11, 12, 13]
'\v\f\r'
iex> [104, 101, 108, 108, 111]
'hello'
出力可能な ASCII ナンバーが見つかると、Elixir はそれらを(文字リテラルのリスト)文字で出力します。
iex> i 'hello'
Term
'hello'
Data type
List
Description
...
Raw representation
[104, 101, 108, 108, 111]
Reference modules
List
Implemented protocols
...
Elixir においてシングルクォーテーションとダブルクォーテーションは同等ではなく、異なる型であるということを念頭においてください。
iex> 'hello' == "hello"
false
シングルクォーテーションは文字リストであり、ダブルクォーテーションは文字列です。これについては “バイナリ、文字列、文字リスト” の章に譲ります。
タプル
タプルを作るには波括弧を使用して記述します。タプルはリストと同じように要素を持ちます。
iex> {:ok, "hello"}
{:ok, "hello"}
iex> tuple_size {:ok, "hello"}
2
タプルの要素はメモリ上で隣接して保存されます。これによって、インデックスで要素にアクセスしたりタプルのサイズを得ることが高速になります。インデックスはゼロから始めます。
iex> tuple = {:ok, "hello"}
{:ok, "hello"}
iex> elem(tuple, 1)
"hello"
iex> tuple_size(tuple)
2
put_elem/3
を用いてタプル内の特定のインデックスに要素を追加することも出来ます。
iex> tuple = {:ok, "hello"}
{:ok, "hello"}
iex> put_elem(tuple, 1, "world")
{:ok, "world"}
iex> tuple
{:ok, "hello"}
put_elem/3
は新規にタプルを返します。オリジナルのタプルは要素を改変されることなく依然として tuple
に保存されています。リストと同様にタプルもイミュータブルです。タプルに対する各操作は新規にタプルを作成することになりますので、元のタプルが改変されることはありません。
リスト or タプル
リストとタプルの違いは何でしょう?
リストはメモリ上で連結リストとして保持されます。これは、そのリストが終わりに達するまで、リスト内の各要素がその値とそれに次ぐ要素へのポインタを保持しているという事です。例えば、リストのサイズなどを数え上げる際にリスト内を走査するといったような、リストの長さに対してアクセスする線形演算を想定しています。
同様に、リストを連結する際のパフォーマンスは、左側のリストの長さに依存します。
iex> list = [1, 2, 3]
# `[0]` を走査だけで `list` の先頭へ追加できるので高速
iex> [0] ++ list
[0, 1, 2, 3]
# `4` を後尾へ追加する為に `list` の走査が必要なので低速
iex> list ++ [4]
[1, 2, 3, 4]
一方でタプルは、メモリ内で隣接して保持されます。それは、タプルのサイズを得たりインデックスから要素にアクセスするのが高速だということです。しかし、タプルに要素を追加したり更新するには、新たにメモリ内でタプルを作り直さねばならないという高いコストがかかります。
iex> tuple = {:a, :b, :c, :d}
iex> put_elem(tuple, 2, :e)
{:a, :b, :e, :d}
ただし、新たに作り直されるのはタプルそれ自身であって、その中身ではないことに注意してください。それは例えば、タプルを更新しても置き換えられたエントリーを除いたすべてのエントリーが古いタプルと新しいタプルで同じということです。言い換えると、 Elixir におけるリストとタプルはそれらコンテンツを共有可能ということを意味しており、言語が機能を果たす為に確保するメモリ量を抑えてくれます。それが可能なのも、この言語のイミュータブルなセマンティクスのおかげです。
それら動作特性はデータ構造の慣習に影響します。タプルを使用した最も一般的な例のひとつに、関数から特殊な情報を得る為に使うということが挙げられます。例えば File.read/1
はファイル内のコンテンツを読み出す関数ですが、その戻り値はタプルです。
iex> File.read("path/to/existing/file")
{:ok, "... contents ..."}
iex> File.read("path/to/unknown/file")
{:error, :enoent}
File.read/1
に与えられたパスが存在しているのなら、最初の要素を :ok
としてそれに次ぐ二つ目にファイル内のコンテンツを含むタプルを返します。
殆どの場合、 Elixir は正しく動作する為にあなたをガイドするようになっています。例えば、 elem/2
はタプルの要素にアクセスする為の関数ですが、リストに対するそれと等価のものは組み込まれていません。
iex> tuple = {:ok, "hello"}
{:ok, "hello"}
iex> elem(tuple, 1)
"hello"
データ構造の要素を数え上げるといった操作をする時、Elixir の関数名は次のルールに従って命名されます。至ってシンプルなものです。その操作が一定時間内のもの(i.e. 値が事前計算されているなど)であれば size
を添え、その操作が線形のもの(i.e. 入力値を得る毎に遅くなるような長さについて計算するなど)であれば length
を添えます。
例えば、次に挙げる 4 つのカウント機能を持った関数などがそうです。 byte_size/1
(文字数を得る) 、 tuple_size/1
(タプルの長さを得る) 、 length
(リストの長さを得る) 、そして String.length/1
(文字数を得る) です。ある文字列の数を得る byte_size
、これは Unicode 文字の数を取得します。別の手段として String.length
を使えますが、文字列全体の走査に依存するので高いコストがかかります。
他にも、 Elixir は Port
、 Reference
、 PID
というデータ型も提供しており、プロセス通信で頻繁に利用されます。プロセスについてお話しする際には簡単に取り上げますが、とりあえず今は基本的な型の扱い方を見ていきましょう。