loading...
gumi TECH Blog

Elixir入門 02: 型の基本

gumitech profile image gumi TECH Updated on ・6 min read

本稿はElixir公式サイトの許諾を得て「Basic types」の解説にもとづき、加筆補正を加えてElixirの基本的なデータ型についてご説明します。採り上げる型と値の例は、つぎのとおりです(表001)。

表001■型と値の例

基本的な型 値の例
整数 1
整数(16進数) 0x1F
小数 1.0
論理値 true
アトム :atom
文字列 "elixir"
リスト [1, 2, 3]
タプル {1, 2, 3}

基本的な数値

整数は四則演算できます。

iex> (1 + 2) * 3 - 4
5

ただし、除算(演算子/)は結果が整数であっても、浮動小数点数に変わります。

iex> 10 / 2
5.0

整数の割り算で、商の整数部を得るのはdiv関数です。関数の引数はかっこ()で囲んでも構いませんし、省くこともできます。なお、整数の割り算の余り(剰余)を求めるのはrem関数です。

iex> div(10, 2)
5
iex> div 10, 2
5
iex> rem 10, 3
1

Elixirでは名前のついた関数のかっこ()は省けるので、宣言や制御の流れの組み立てが見やすい構文で書けます。

整数には接頭辞をつけて、2進数(0b)や8進数(0o)あるいは16進数(0x)として扱えます。

iex> 0b1000
8
iex> 0o10
8
iex> 0xFF
255

小数として扱う数値には、小数点をつけます。小数点以下の数値は省けません。端数がないときには、0を添えてください。あるいは、eを用いた浮動小数点数の形式で表すこともできます。

iex> 1.0
1.0
iex> 3.14e-2
0.0314

Elixirの浮動小数点数値は64ビットの倍精度です。

関数roundtruncを使えば、小数点以下の四捨五入や切り捨てができます。

iex> round(2.718)
3
iex> trunc(2.718)
2

関数の表記

Elixirでは、関数は名前と受け取る引数の数によって定まります。引数の数はアリティと呼ばれます。そこで、ドキュメントで関数は「関数名/アリティ」のように表されるのです。たとえば、前出の数値を四捨五入する関数ならround/1です。名前が同じでも引数の数が異なれば、別の関数として扱われることになります。

論理(ブーリアン)値

Elixirには、truefalseの2値をもつ論理(ブーリアン)型があります。

iex> true
true
iex> 1 > 2
false

データ型を調べるためには、型に応じたさまざまな関数があります。論理値かどうか確かめるのはis_boolean/1関数です。truefalseかの論理値を返します。

iex> is_boolean(1 > 2)
true
iex> is_boolean(1)
false

数値についてはほかにも、関数is_integer/1(整数)やis_float/1(浮動小数点数値)さらにis_number/1(数値)などが備わっています。

なお、iexではh()でヘルプが表示されます。たとえば、is_number/1関数について知りたいときは、つぎのように打ち込んでください(アリティは省くこともできます)。関数の構文と説明が示されるでしょう(図001)。

iex> h is_number/1

図001■iexに示された関数の構文

elixir_01_001.png

アトム

アトムは名前がそのまま値となる定数です。言語によっては、シンボルと呼ばれることもあります(たとえばRuby)。アトムは名前の前にコロン:を添えます。

iex> :hello
:hello
iex> :hello == :world
false

論理値のtruefalseは、アトムとして備わっています。値がアトムか、また論理値かは、それぞれ関数is_atom/1is_boolean/1で調べられます。

iex> true == :true
true
iex> is_atom(false)
true
iex> is_boolean(:false)
true

Elixirのモジュール名もアトムです。

iex> is_atom(String)
true

カスタムのモジュールを定めるとき、名前は大文字ではじめます。たとえ実際にはモジュールがなくても、頭文字が大文字の識別子はアトムとみなされます。

iex> is_atom(MyModule)
true

また、Erlangのモジュールを参照するときに、アトムが用いられます。たとえば、Erlangには数学的な関数を納めたmathモジュールが備わっています。これはアトム:mathとして参照できるのです。

iex(6)> :math.pow(2, 3)
8.0
iex(7)> is_atom(:math)
true

文字列

Elixirでは文字列は、ダブルクォーテーション("")にかこんで示します。エンコーディングはUTF-8です。

iex> "hello"
"hello"
iex> "拝啓"
"拝啓"

アトムは#{}でかこんで文字列補間することにより、文字列に含めることができます(「Elixir の危険な構文、atom文字列補間」参照)。

iex> "hello #{:world}"
"hello world"

文字列には改行が含められます。また、エスケープシーケンス(\n)で加えてもかまいません。

iex> "hello
...> world"
"hello\nworld"
iex> "hello\nworld"
"hello\nworld"

文字列を出力するには、IOモジュールのIO.puts/2関数を使います。加えて示されるアトム:okは、正しく出力できたときの戻り値です。

iex> IO.puts "hello\nworld"
hello
world
:ok

Elixirは文字列をバイトシーケンスのバイナリとして扱います。バイナリであることを確かめる関数はis_binary/1です。

iex> is_binary("hello")
true

byte_size/1関数を用いれば、バイト数が調べられます。

iex> byte_size("hello")
5
iex> byte_size("拝啓")
6

UTF-8では、バイト数は必ずしも文字数を表しません。文字数を得るために使うのはString.length/1関数です。

iex(16)> String.length("hello")
5
iex(17)> String.length("拝啓")
2

無名関数

Elixirでは、関数は他の関数に引数として渡したり、戻り値として返すことができる「第一級関数」(first-class function)です。そのような場合、関数に名前をつけずに定めることが少なくありません。あとからその関数を呼び出したいときは、変数に納めます。

関数はキーワードfnではじめ、endで閉じます。引数はかっこ()の中にカンマ(,)区切りで与え、つづけて添えた->のあとに本体の文を書きます。なお、引数をかこむ()は省いてもかまいません。正しく定められると、#Function<...>の記述が示されます(<>内は環境によって異なります)。

iex> sum = fn (a, b) -> a + b end
#Function<12.99386804/2 in :erl_eval.expr/5>

呼び出すときは、関数が納められた変数を参照し、ドット(.)につづけてかっこ()に引数をカンマ区切りで渡します。なお、この場合の()は省けません。

iex> sum.(1, 2)
3

変数のあとにドット(.)を添えるのは、名前のある関数でないことを示すためです。Elixirでは、名前のある関数と無名関数とを区別します。

変数に関数が納められているかどうかは、is_function/1関数でたしかめられます。

iex> is_function(sum)
true

さらに、アリティの数まで調べられるのがis_function/2関数です。アリティ数を第2引数に渡します。

iex> is_function(sum, 1)
false
iex> is_function(sum, 2)
true

無名関数はキャプチャ演算子&を用いて、短く書き表すことができます(「Function capturing」参照)。&に1からの整数連番を添えて示されるのが引数です。

iex> sum = &(&1 + &2)
&:erlang.+/2
iex> sum.(3, 4)
7

無名関数はクロージャです。みずからの定められたスコープの中で参照を解決します。すでに変数に納めた関数を、別の関数から参照して使うこともできます。

iex> double = fn (a) -> sum.(a, a) end
#Function<6.99386804/1 in :erl_eval.expr/5>
iex> double.(2)
4

無名関数は変数に納めなくても、ただちに呼び出すことはできます。なお、関数内で割り当てられた変数が、外の環境に影響を与えることはありません。

iex> x = 0
0
iex> (fn -> x = 1 end).()
1
iex> x
0

リスト

いくつもの値をひとつにまとめたのがリストです。角かっこ[]の中に値(要素)をカンマ区切りで与えます。値はどのような型でもかまいません。リストの長さ(要素の数)は、length/1関数で調べられます。

iex> list = [3.14, :pie, true, "Apple"]
[3.14, :pie, true, "Apple"]
iex> length(list)
4

演算子++/2--/2を用いて、別のリストの要素を加えたり、差し引いたりすることができます。

iex> list ++ ["Cherry"]
[3.14, :pie, true, "Apple", "Cherry"]
iex> ["π"] ++ list
["π", 3.14, :pie, true, "Apple"]
iex> list -- [true, false]
[3.14, :pie, "Apple"]

重複した値が含まれるリストから複数の同じ値を差し引くときは、左にある値から順に除かれます。このとき差し引く値は、厳密な等価でマッチしなければなりません(「Elixir入門 03: 演算子の基本」「比較」参照)。

iex> [1, 1, 2, 1, 2, 3, 1, 2, 3, 4] -- [4, 1, 3, 1, 2]
[1, 2, 1, 2, 3]

リスト演算子は、参照したリストそのものの中身は変えません。返されるのは新たなリストです。Elixirは「イミュータブル」(immutable)なデータ構造をもちます。データが書き替えられることはなく、別のデータに変換することしかできません。それにより、クリーンなコードが保てるのです。

リストを扱うときは、たびたびヘッド(head)とテイル(tail)を取り出します。ヘッドが最初の要素、テイルは残りの要素を納めたリストです。それぞれ、関数hd/1tl/1で得られます。

iex> hd(list)
3.14
iex> tl(list)
[:pie, true, "Apple"]

リストが空(長さ0)のときは、引数エラー(ArgumentError)になります。

iex> hd([])
** (ArgumentError) argument error
    :erlang.hd([])
iex> tl([1])
[]
iex> tl([])
** (ArgumentError) argument error
    :erlang.tl([])

|演算子を用いたパターンマッチングにより、ヘッドとテイルをふたつの変数に与えることもできます(「Pattern matching」参照)。

iex> [head | tail] = list
[3.14, :pie, true, "Apple"]
iex> head
3.14
iex> tail
[:pie, true, "Apple"]

整数のリストをつくったとき、シングルクォーテーション('')にかこわれた整数でない値で返されることがあります。

iex> [10, 11, 12]
'\n\v\f'
iex> [104, 101, 108, 108, 111]
'hello'

Elixirは出力できるASCIIコードのリストを、コードに対応した文字リスト(charlist)として示すのです。charlistはErlangのコードとのインタフェースでよく用いられます(「Charlists」参照)。シングルクォーテーションにかこわれたcharlistの情報は、iexのi/1関数で調べられます。

iex> i('hello')
Term
  'hello'
Data type
  List
Description
  This is a list of integers that is printed as a sequence of characters
  delimited by single quotes because all the integers in it represent valid
  ASCII characters. Conventionally, such lists of integers are referred to
  as "charlists" (more precisely, a charlist is a list of Unicode codepoints,
  and ASCII is a subset of Unicode).
Raw representation
  [104, 101, 108, 108, 111]
Reference modules
  List
Implemented protocols
  IEx.Info, Inspect, String.Chars, List.Chars, Collectable, Enumerable

Elixierでは、同じ文字列でも、シングルクォーテーションとダブルクォーテーションのどちらでかこうかにより扱いが異なります。ふたつのデータが、それぞれ別の型だからです。

iex> 'hello' == "hello"
false
iex> i("hello")
Term
  "hello"
Data type
  BitString
Byte size
  5
Description
  This is a string: a UTF-8 encoded binary. It's printed surrounded by
  "double quotes" because all UTF-8 encoded codepoints in it are printable.
Raw representation
  <<104, 101, 108, 108, 111>>
Reference modules
  String, :binary
Implemented protocols
  IEx.Info, Inspect, String.Chars, List.Chars, Collectable

タプル

タプルもリストと同じく、複数の値がまとめて納められます。値はどのような型でもかまいません。波かっこ{}の中に値(要素)をカンマ区切りで与えます。長さ(要素数)を調べる関数はtuple_size/1です。

iex(5)> tuple = {3.14, :pie, true, "Apple"}
{3.14, :pie, true, "Apple"}
iex(6)> tuple_size(tuple)
4

タプルは、要素をメモリに連続して納めます。そのため、長さはリストより速く得られます。またelem/2関数を用いて、0からはじまる整数連番のインデックスで、要素を取り出すこともできるのです。

iex> elem(tuple, 3)
"Apple"

put_elem/3関数で、指定したインデックスの値を置き替えることもできます。ただし、リストと同じく、タプルもイミュータブルです。操作をして返されるのは新たなタプルで、参照もとのデータは変わりません。

iex> put_elem(tuple, 3, "Cherry")
{3.14, :pie, true, "Cherry"}
iex> tuple
{3.14, :pie, true, "Apple"}

リストとタプルの違い

リストもタプルもデータの型を問わず、値がいくつでも要素として納められました。では、ふたつの違いは何でしょう。

リストは「連結リスト」です。要素は値とつぎの要素への参照をもちます。そのためリストの長さは、要素をはじめから順に最後までたどって数えないとわからないのです。

また、別のリストを加えるときも、++/2演算子の左オペランドのリストが長いほど遅くなることになります。左オペランドのリスト要素の最後に、右オペランドのリスト最初の要素の参照を与える操作だからです。

iex> list = [1, 2, 3]
# 左オペランドの長さが1なので速い
iex> [0] ++ list
[0, 1, 2, 3]
# 左オペランドの長さが3なので遅い
iex> list ++ [4]
[1, 2, 3, 4]

タプルは、値を連続してメモリに納めます。そのため、サイズを得たり、インデックスで値を取り出す操作も速いのです。けれども、要素を加えたり、書き替えたりする場合は、メモリに新たなタプルをつくることになるので、負荷が高くなってしまいます。

iex> tuple = {:a, :b, :c, :d}
iex> put_elem(tuple, 2, :e)
{:a, :b, :e, :d}

これは、タプルの構造のお話でした。中の要素については、タプルもリストもデータは共有します。たとえば、ひとつの要素を書き替えたことでつくられる新しいタプルは、更新された要素を除いて、古いタプルとデータは共有するのです。プログラムが費やすメモリはその分抑えられます。イミュータブルな言語の恩恵といえるでしょう。

パフォーマンスの性質によりデータ構造の使い方が決まります。タプルがよく用いられるのは、関数の戻り値として情報を返すときです。たとえば、File.read/1関数は、ファイル読み込みの結果と内容をタプルで返します。

iex> File.read("path/to/existing/file")
{:ok, "... ファイルの中身 ..."}
iex> File.read("path/to/unknown/file")
{:error, :enoent}

関数の引数に渡したパスが読み込めれば、戻り値のタプルのひとつ目の要素はアトム:okで、ふたつ目がファイルの中身です。読み込めなかったときは、戻り値の第1要素はアトム:errorで、第2要素がエラー内容を示します。

また、タプルはelem/2関数で、指定したインデックスの要素が得られます。リストにはこの機能が組み込まれていません。

iex> tuple = {:ok, "hello"}
{:ok, "hello"}
iex> elem(tuple, 1)
"hello"

データ構造の中の要素を数える関数について、Elixirは名前のつけ方を決めています(表002)。あらかじめ計算された値を返すのがsizeのつく関数です。数えて返す関数にはlengthがつきます。長さを調べて返すので、数が増えるほど処理は遅くなるのです。

表002■sizeとlengthの用語の違い

使われる語 値の求め方 処理
size あらかじめ計算されている 速い
length 数えて返す 遅い

つぎの4つの関数をすでにご紹介しました。同じ文字列に対する操作でも、バイト数を得るbyte_size/1は速い処理です。ところが、Unicodeの文字数を調べるString.length/1は、文字列が長くなるほど処理には時間がかかってしまいます。

  • byte_size/1: 文字列のバイト数
  • tuple_size/1: タプルの大きさ
  • length/1: リストの長さ
  • String.length/1: 文字列の文字数

Elixir入門もくじ

番外

Posted on by:

gumitech profile

gumi TECH

@gumitech

gumi TECH は、株式会社gumiのエンジニアによる技術記事公開やDrinkupイベントなどの技術者交流を行うアカウントです。 gumi TECH Blog: http://dev.to/gumi / gumi TECH Drinkup: http://gumitech.connpass.com

gumi TECH Blog

株式会社gumiのエンジニアによる技術記事を公開しています。

Discussion

markdown guide