Skip to content

最初のプロジェクト

Hello world を書いて、実行、検査、テスト、整形を一通り試します。

Terminal window
mkdir -p hello/src
cd hello
touch Pome.toml
hello/
├── Pome.toml
└── src/
└── main.emel

Pome.toml を置いたディレクトリがプロジェクトです。中身は空でかまいません。emela は渡されたパスから祖先をたどり、最初に見つかった Pome.toml のディレクトリをプロジェクトにします。書ける内容(名前、版、依存、既定のターゲット)は プロジェクトと Pome にあります。

ソースは src/ の下に置き、1つのファイルが1つのモジュールになります。src/greeting.emel は Greeting として import できます。エントリは src/main.emel の main です。

src/main.emel:

import Std.Console.{Console}
import Std.Platform.{Platform}
fn greeting(name: String) -> String {
"Hello, #{name}!"
}
fn main() {
with Platform {
console = use Console
console.print_line(greeting("world"))
}
}
@test
fn greets_by_name() {
assert greeting("Emela") == "Hello, Emela!"
}

画面への出力も副作用なので、エフェクト Console の操作として呼びます。use Console で能力の値を取り出し、console.print_line(...) と書きます。

with Platform { ... } は、本体の中で使う Console、Clock、FileSystem などのホストのハンドラをまとめて供給します。main の型に残ってよいエフェクトはありません。with を外すと、Console が供給されていないというエラーになります。Console だけで足りるなら、import Std.Console.{Console, SystemConsole} として with SystemConsole { ... } と書いても同じです。

Terminal window
$ emela run
Hello, world!

emela run はプロジェクトを JavaScript にコンパイルして Node.js で実行します。パスを省くと . を対象にします。出力は target/emela/js に書き出され、main.mjs がエントリです。

target/emela/js/
├── emela_core/std/...
├── emela_runtime.mjs
└── main.mjs

コンパイルだけなら emela build です。出力先は同じ target/emela/js で、--out-dir で変えられます。

emela check はコンパイルせずに診断だけを出します。問題がなければ何も出さずに終了コード 0 で終わります。型を間違えると、こうなります。

fn main() {
n: Int = "three"
dbg(n)
}
Terminal window
$ emela check
error[E0301]: type mismatch: expected `Int`, found `String`
╭─[ src/main.emel:2:12 ]
│
2 │ n: Int = "three"
│ ───┬───
│ ╰─────
───╯
1 error

@test を付けた引数のない関数がテストです。emela test が1つずつ実行します。

Terminal window
$ emela test
running 1 test
test greets_by_name ... ok
test result: ok. 1 passed; 0 failed; 0 filtered out

失敗すると、assert の式と両辺の値が出ます。

Terminal window
$ emela test
running 1 test
test fails_on_purpose ... FAILED
failures:
---- fails_on_purpose ----
defect: assertion failed: 1 + 1 == 3
left: 2
right: 3
failures:
fails_on_purpose
test result: FAILED. 0 passed; 1 failed; 0 filtered out

emela test . greets のように2つ目の引数を渡すと、名前にその文字列を含むテストだけを実行します。テストの出力は target/emela/test に書かれ、emela build の出力は上書きしません。書き方は テスト にあります。

emela fmt は src/ の下の .emel を書き換えて整形します。--check を付けると書き換えず、整形が要るファイルを一覧にして終了コード 1 で終わります。CI ではこちらを使います。

Terminal window
$ emela fmt --check
src/extra.emel
$ emela fmt

dbg(x) は x を標準エラーに出し、x をそのまま返します。式の途中に挟めます。

fn main() {
xs = [1, 2, 3]
dbg(xs)
total = dbg(List.fold(xs, 0, fn(acc, x) { acc + x }))
dbg("total: #{total}")
}
Terminal window
$ emela run main.emel
[1, 2, 3]
6
"total: 6"

表示は値の構造そのままで、文字列は引用符付き、レコードは User(id: 1, name: "alice") の形になります。標準エラーに出るので 2>/dev/null で消せます。プログラムの出力には Console を使います。main が返す値は表示されません。

Pome.toml のないディレクトリでは、ファイルを1つ渡すだけで動きます。上の dbg の例はこの形で動かしています。

Terminal window
$ emela run main.emel
$ emela check main.emel
$ emela test main.emel

渡したファイルがエントリで、import できるのは同じディレクトリの直下にある .emel だけです。出力はファイルのあるディレクトリの target/emela/js(テストは target/emela/test)に書かれます。

このサイトの例も、単独のファイルとしてコンパイラの検査を通しています。

状況終了コード
正常に終わった0
診断にエラーがある1
期待されるエラーが main まで伝わった1
defect で終わった101
中断したまま再開されずに終わった130

main まで伝わったエラーは、標準エラーに error: <値> と出ます。

error Invalid(reason: String)
fn check_age(age: Int) {
if age < 0 { fail Invalid(reason: "negative age") }
age
}
fn main() {
dbg(check_age(20))
dbg(check_age(-1))
}
Terminal window
$ emela run main.emel
20
error: Invalid(reason: "negative age")
$ echo $?
1

defect は defect: <メッセージ> と出ます。

fn main() {
dbg(1 / 0)
}
Terminal window
$ emela run main.emel
defect: division by zero
$ echo $?
101

エラーと defect の違いは エラー に、コマンドの全体は emela コマンド にあります。