第2章 値・型・式
第1章では駆け足で言語を1周しました。ここからの第II部では、部品を一つずつ手に取って眺めます。まずは一番小さな部品 — 値と、その型と、それらを組み合わせる式です。
この章でも REPL を開いておいてください。Lune の式はすべて REPL にそのまま打ち込めます。「この式の型は何だろう」と思ったら、打てば答えが返ります。
2.1 リテラルと基本型
Lune のリテラルと、対応する型の一覧です。
| リテラル | 型 | 例の表示 |
|---|---|---|
42 | Int | 42 : Int |
3.14 | Double | 3.14 : Double |
"hello" | String | "hello" : String |
true / false | Bool | true : Bool |
null | Null | null : Null |
() | Unit | () : Unit |
(1, "a") | Tuple[Int, String] | (1, "a") : Tuple[Int, String] |
いくつか補足します。
Int は大きさを気にしなくてよい。v0.1 の Int は任意精度で、あふれることがありません。
lune> 9223372036854775807 + 1
9223372036854775808 : Int
64ビット整数の限界を超えても平気です(C ならここで音もなく負の数になります)。
文字列のエスケープ。" の中では \n(改行)や \" が使えます。表示(show 形式)ではエスケープされたまま出るので驚かないでください。
lune> "a\nb"
"a\nb" : String
これはあくまで REPL の show 形式の話です。println に渡すと生の内容が出力されるので、\n は実際の改行になります(1.3節で見たとおりです)。
シングルクォートは String になる。'x' という書き方も受け付けますが、v0.1 に独立した文字型は事実上なく、"x" と同じ String になります。
lune> 'x'
"x" : String
null と () は別物です。null は「値がない」ことを表すためだけの値で、null 安全(第7章)の主役です。() は Unit 型のただ一つの値で、「返すべき意味のある値がない」関数(println など)の戻り値に使われます。
2.2 演算子 — 算術・比較・論理
算術演算子は + - * / // % と単項の - です。* / // % は同じ優先順位で、それより + - が弱い — おおむね数学の直感どおりで、() でくくれば変えられます。
lune> 1 + 2 * 3
7 : Int
lune> (1 + 2) * 3
9 : Int
lune> 2 * -3
-6 : Int
割り算には注意が要ります。第1章でも触れたとおり、/ は常に Double を返します。Int を Int で割っても Double です。整数の割り算の余りが欲しいときは %(こちらは Int)です。
lune> 7 / 2
3.5 : Double
lune> 7 % 2
1 : Int
では「整数を整数で割って整数が欲しい」ときは何を使うのか。床除算 // です。結果の型はオペランドの型のまま変わりません。
lune> 7 // 2
3 : Int
lune> 7.0 // 2.0
3.0 : Double
/ と // は名前が似ていますが、別の演算子です。「割り算」と言うとき Lune には2つある、と覚えてください。Int のまま計算を続けたいなら //、実数として割りたいなら / です。
// の丸めには一つ、予想を裏切るところがあります。負の無限大方向への切り下げ(floor)で、ゼロ方向への切り捨てではありません。
lune> -7 // 2
-4 : Int
-3.5 を「切り捨て」て -3 だと思った人は、ここで一度つまずきます。なぜ -4 なのかは、% と組で辻褄が合っているからです — その理由は第13章の演習 13-4 でゆっくり考えます。いまは「// は floor」と覚えておけば十分です。
なお / // % はどれも、右側が 0 なら実行時エラーになります(RUN0006。第11章で読み方を学びます)。
そして Int と Double は黙って混ざりません。
lune> 1 + 2.0
error[TYP0003]: +: Int が必要ですが、Double が見つかりました
= help: 詳しくは `lune explain TYP0003 --lang ja` を実行してください
lune> 1 == 1.0
error[TYP0003]: ==: Int と Double は比較できません
= help: 詳しくは `lune explain TYP0003 --lang ja` を実行してください
暗黙の型変換で「だいたい合ってる」結果を返すより、書き手にどちらの世界で計算したいのかを決めてもらう — Lune はその立場です。2.0 か 2 か、リテラルの側を揃えてください。
比較演算子は == != < <= > >= です。== と != は数値・文字列・Bool で使えます。大小比較の < 系は数値専用です。
lune> 3 < 5
true : Bool
lune> "a" == "a"
true : Bool
lune> "a" < "b"
error[TYP0003]: <: 数値型が必要ですが、String が見つかりました
= help: 詳しくは `lune explain TYP0003 --lang ja` を実行してください
複合値 — タプル、リスト、そして第5章で学ぶ ADT の値 — も == で比べられます。比較は構造的です。別々に作った値でも、中身が等しければ等しい値です。
lune> (1, 2) == (1, 2)
true : Bool
lune> (1, 2) == (2, 1)
false : Bool
lune> [1, 2] == [1, 2]
true : Bool
lune> Some(1) == Some(1)
true : Bool
ただし、比べられるのは同じ型どうしだけです。要素数の違うタプルは中身以前に型が違うので、比較そのものが型エラーになります。
lune> (1, 2) == (1, 2, 3)
error[TYP0003]: ==: Tuple[Int, Int] と Tuple[Int, Int, Int] は比較できません
= help: 詳しくは `lune explain TYP0003 --lang ja` を実行してください
論理演算子は &&(かつ)と ||(または)、否定は関数 not です。&& は || より強く結びつきます。そして第4章で見た遅延評価のおかげで、どちらも短絡します — 左だけで答えが決まるなら、右は評価されません。
lune> false && crash()
false : Bool
lune> true || crash()
true : Bool
lune> not(true)
false : Bool
このほかに null 合体の ??(第7章)とパイプライン |>(第3章)があります。全演算子の優先順位表は付録Aにまとめます。
2.3 if は式 — then/else と elif
第1章の復習から。if は値を返す式で、1行形式とブロック形式があります。
lune> if 5 > 3 then "big" else "small"
"big" : String
式である以上、守るべき規則が2つあります。条件は Bool、そして分岐の型は一致です。
lune> if 1 then 2 else 3
error[TYP0003]: if の条件: Bool が必要ですが、Int が見つかりました
= help: 詳しくは `lune explain TYP0003 --lang ja` を実行してください
lune> if true then 1 else "a"
error[TYP0003]: 分岐の型が一致しません: Int と String
= help: 詳しくは `lune explain TYP0003 --lang ja` を実行してください
C のように「0 は偽」ではありません。if x % 2 ではなく if x % 2 == 1 と書きます。
分岐が増えるときは、ブロック形式の elif が使えます。grade.lune:
module grade
# if は式。block 形式では elif で分岐を足せる。
def grade(score: Int): String =
if score >= 80:
"pass"
elif score >= 60:
"retry"
else:
"fail"
let result = grade(75)
$ lune --eval result grade.lune
"retry"
どんなに分岐しても、grade は必ず String を一つ返します。「どの経路を通っても値がある」ことを型検査が保証してくれる — 式指向の安心感です。
2.4 let-in — 式の中の束縛
let はトップレベルだけのものではありません。式の中で一時的な名前が欲しいときは let ... in ... が使えます。
lune> let answer = let x = 40 in x + 2
ok
lune> answer
42 : Int
in の右側が式全体の値になり、x はその中でだけ見えます。名前を2つ以上付けたいときは、ブロック形式で let を並べるのがきれいです。
module answers
# 演習 2-3: ブロック形式の let で中間値に名前を付けて BMI を計算する。
let bmi =
let h = 1.7
let w = 62.0
w / (h * h)
$ lune --eval bmi ex2-3.lune
21.453287197231838
インデントされたブロックの中に let を並べ、最後に置いた式がブロック全体の値になります。lune fmt もこの形を正準としています。
2.5 型注釈 — 書く場所と省ける場所
let には型注釈を付けられます。
let y: Int = 42
注釈と実際の型が食い違えば、もちろん型エラーです。annot.lune:
module bad
let n: Int = "hello"
$ lune --check annot.lune
error[TYP0003]: let の型注釈: Int が必要ですが、String が見つかりました
--> annot.lune:3:14
|
3 | let n: Int = "hello"
| ^^^^^^^ この式の型は String
= help: 詳しくは `lune explain TYP0003 --lang ja` を実行してください
では、どこで注釈を書き、どこで省けるのか。v0.1 の目安はこうです。
| 場所 | 注釈 |
|---|---|
let の右辺が明らか | 省略できる(右辺から推論される) |
def の引数と戻り値 | 書く(再帰関数の戻り値型は必須 — TYP0011) |
ラムダ fn x -> ... の引数 | 文脈から推論されることが多い(第3章) |
注釈は機械のためだけのものではありません。let total: Double = ... と書いておけば、読む人への宣言になり、右辺を書き間違えた瞬間に上のような診断が出ます。迷ったら書く、確かめたいときは :type — これが実務の感覚です。
2.6 タプル — 型の違う値の組
リスト(第8章)の要素はすべて同じ型ですが、タプルは型の違う値を決まった個数だけ束ねます。
lune> let pair = (1, "a")
ok
lune> pair
(1, "a") : Tuple[Int, String]
タプルから値を取り出すのは分解束縛です。.1 のようなアクセサはありません — 名前を付けて開けます。
lune> let (a, b) = (10, 20)
ok
lune> a
10 : Int
lune> b
20 : Int
第1章の温度換算表では (華氏, 摂氏) のタプルのリストを作りました。「2つの値をとりあえず束ねて運ぶ」のがタプルの仕事です。束ねた値に名前を付けたくなったら、それはレコード(第6章)の出番です。
壊してみよう — 型検査より前の段階、字句とレイアウトのエラーも見ておきましょう。どちらも第11章で学ぶ診断コード体系の「LXL」「PRS」族です。
let x = $1と書くと、Lune の知らない文字でエラーになります。error[LXL0001]: 予期しない文字 '$' --> lex.lune:3:9 | 3 | let x = $1 | ^ 予期しない文字 = help: 詳しくは `lune explain LXL0001 --lang ja` を実行してください理由なく行頭を字下げすると、レイアウト規則に引っかかります。
error[PRS0001]: トップレベル宣言が必要ですが、INDENT が見つかりました --> indent.lune:4:1 | 4 | let b = 2 | ^ 予期しないトークン = help: 詳しくは `lune explain PRS0001 --lang ja` を実行してくださいLune のインデントは Python と同じく構文の一部です。ブロックを作るのは
=や:の後だけ、と覚えてください。
まとめ
| 事実 | 一言で |
|---|---|
| 基本型 | Int(任意精度)/ Double / String / Bool / Null / Unit / Tuple[...] |
| 割り算 | / は常に Double、// は床除算で型はそのまま、余りは % |
// の丸め | 負の無限大方向(floor)。-7 // 2 は -4 |
Int と Double | 暗黙には混ざらない。リテラルの側を揃える |
== | 中身(構造)で比較。比べられるのは同じ型どうしだけ |
&& ` | |
if | 条件は Bool、分岐の型は一致。elif で多分岐 |
let ... in / ブロック let | 式の中の一時的な名前 |
| 型注釈 | let では省略可、def では書く。確認は :type |
| タプル | (a, b) で束ね、let (x, y) = ... で開ける |
演習問題
演習 2-1(★) 次の式の値と型を予想してから、REPL で確かめてください。
19 / 4
19 // 4
19 % 4
1 + 2 * 3 - 4
2 * -3
true && false || true
解答
4.75 : Double(/ は常に Double)、4 : Int(// は床除算なので型は Int のまま)、3 : Int、3 : Int、-6 : Int、true : Bool(&& が先に評価されて false、次に || true)。
演習 2-2(★) grade.lune に「90 点以上は “excellent”」の分岐を足してください。分岐をどこに足すかで結果が変わることを、grade(95) で確かめてください。
解答
if score >= 90: "excellent" を先頭に置きます。elif score >= 80: ... より後ろに置くと、95 点は先に score >= 80 に捕まって "pass" になってしまいます。if/elif は上から順に試される — 条件の並び順は仕様の一部です。
演習 2-3(★★) 身長 1.7 m、体重 62.0 kg の BMI(体重 ÷ 身長²)を、ブロック形式の let で中間値に名前を付けながら計算してください。
解答
module answers
# 演習 2-3: ブロック形式の let で中間値に名前を付けて BMI を計算する。
let bmi =
let h = 1.7
let w = 62.0
w / (h * h)
$ lune --eval bmi ex2-3.lune
21.453287197231838
1.7 を 1 にすると TYP0003 が出ます。Int と Double の分離は、こういう単位の混同を早めに捕まえてくれます。
演習 2-4(★★) 分解束縛を使って、タプル (1, 2) の2つの値を入れ替えた (2, 1) を作ってください。
解答
module answers
# 演習 2-4: タプルの分解束縛で 2 値を入れ替える。
let (x, y) = (1, 2)
let swapped = (y, x)
$ lune --eval swapped ex2-4.lune
(2, 1)
一時変数は要りません。「開けて、逆順に束ね直す」だけです。
演習 2-5(★★) 次の3つの式の結果を予想してから確かめてください。3つ目だけ毛色が違います — 何が起きるでしょうか。
(1, 2) == (2, 1)
[1, 2] == [1, 2]
(1, 2) == (1, 2, 3)
解答
false : Bool(中身の並びが違う)、true : Bool(別々に作ったリストでも中身が同じなら等しい — 構造的等価)、そして3つ目は true/false ではなく型エラーです。
error[TYP0003]: ==: Tuple[Int, Int] と Tuple[Int, Int, Int] は比較できません
= help: 詳しくは `lune explain TYP0003 --lang ja` を実行してください
要素数が違うタプルは違う型なので、「等しいか」を聞くこと自体ができません。== の答えが返るのは同じ型どうしだけ — 「比較できないものは実行前に弾く」という、1 == 1.0 と同じ設計です。
より正確には — リテラルと型は documents/LANGUAGE_SPEC.md §5–6、演算子の全優先順位表は documents/SYNTAX_SPEC.md §14(:: ++ など将来分を含むため、実装済みかは LANGUAGE_SPEC.md を正とします)、if/let-in は documents/LANGUAGE_SPEC.md §9。値の表示規則は documents/VALUE_DISPLAY_SPEC.md。この章のコード例は books/examples/ch02/ にあり、すべて実際の CLI で検証されています。