クックブック
日常的な Yulang のタスクを目的別に引けるレシピ集です。 多くの節では定型的な書き方に短い説明を添え、設定ファイル、ファイル編集、log 集計、もう少し大きいサンプルでは長めの例を扱います。 背景の詳細は言語リファレンスへリンクしています。
例では、C 風の呼び出しより、Yulang らしい 括弧を減らした書き方 を中心に使います。 f x y の bare application、f: ... の colon application、字下げ block です。
値と関数を定義する
my answer = 42
my greet name = "hello, " + name
greet "Yulang"my は private な binding です。 名前のあとに置いた pattern が curry された引数になります。
関数を公開する
pub double x = x + xpub は外部へ export します。 our は囲んでいる companion module 内で見えるようにします(method 定義での既定)。
case で場合分けする
my describe n =
case n:
0 -> "zero"
x if x < 0 -> "negative"
_ -> "positive"arm は上から順に試されます。 guard は pattern の後ろに if を続けて書きます。
struct に method をつける
struct point { x: int, y: int } with:
our p.norm2 = p.x * p.x + p.y * p.y
point { x: 3, y: 4 } .norm2with: は struct の companion module を開きます。 receiver 名 p は .norm2 を呼んだときの値を指します。
role を型に対して実装する
impl Add point:
our a.add b = point { x: a.x + b.x, y: a.y + b.y }role は interface を宣言します。 impl は具体型に対して method を提供します。 receiver a(とここでは第二引数 b)は role の method シグネチャと対応します。
ループから早期脱出する
sub:
for x in 0..:
if x == 5: return x
0sub: は early-return の scope を開きます。 return value は最も近い sub: を抜けます。 for の中では last / next / redo でループそのものを制御します。
$x / &x で局所的な可変参照を使う
{
my $count = 0
&count = $count + 1
&count = $count + 1
$count
}my $x = … は局所の mutable binding を作ります。 $x で読み、&x = value で書きます。 内部的には小さな handled var effect として展開されるので、effect 系の外に逃げず、ちゃんと型に乗ります。
text ファイルを読む
my text = read_text "data.txt"
text.saystr は path に widen されるので、通常の path なら文字列リテラルをそのまま渡せます。 filesystem エラーは effect row の io_err として投げられます。 呼び出し側に値としての result が必要な境界だけ wrap します。
my wrapped = io_err::wrap: read_text "data.txt"
case wrapped:
result::ok text -> text
result::err _ -> ""設定ファイルを読む
設定ファイル、ファイル編集、log 集計の例はexamples/config-file-text/ にあります。
次は一部省略の例です。 設定ファイルを method-form の文字列 API で読み、= の左右を trim してからport を数値化します。
my parse_setting(clean: str): opt (str, str) = case clean.split_once "=":
just (key, value) -> just (key.trim, value.trim)
nil -> nil
my read_config(path: str): (int, list (str, str), int, int) =
my $port = 0
my $entries = []
for line in (std::io::file::read_text path).split "\n":
my clean = line.trim
if clean.starts_with "#":
()
else:
case parse_setting clean:
just (key, value) ->
if key == "port":
case value.to_int:
just parsed -> &port = parsed
nil -> ()
&entries = $entries + [(key, value)]
nil -> ()
($port, $entries, 0, 0)完全な例はconfig_read.yu です。
text ファイルを行単位で編集する
ファイルを durable な text ref として開くと、行単位の編集をそのまま書けます。 次は一部省略の例です。 先に /tmp へ copy を作るので、何度実行しても tracked sample は汚れません。
use std::control::nondet::*
use std::control::var::*
use std::text::str::*
std::io::file::write_text path (std::io::file::read_text source)
my &doc = std::io::file::text path
((&doc.lines.each).update \line ->
line.replace_once "todo:" "done:"
).list
$doc.say完全な例はfile_edit.yu です。
parser pattern で log を集計する
parser pattern は小さい log 集計にも合います。 次は一部省略の例です。 GET payload を capture し、guard で /api だけに絞り、captured milliseconds を int に変換します。
use std::text::parse::*
my api_ms(line: str): opt int = case line:
~"GET {rest = ..}" if rest.starts_with "/api" -> case rest.split_once " took ":
just (_, tail) -> case tail.split_once " ms":
just (ms, _) -> ms.to_int
nil -> nil
nil -> nil
_ -> nil完全な例はlog_stats.yu です。
オプショナル引数を作る
my area {width = 1, height = 2} = width * height
area { width: 3 }
area { width: 3, height: 4 }
area {}default 付きの record pattern は、呼び出し側で各 field を省略できるようにします。 default は左から右へ評価され、後ろの field の default は前の field を参照できます。
型付きエラーを投げて捕まえる
my path = std::text::path::of_bytes (std::text::str::to_bytes "/tmp/data")
catch read_text path:
io_err::not_found _, _ -> "(missing)"
value -> valuefail e は e.throw の薄皮です。 catch arm は operation 名を直接書いてエラーを捕まえます。 エラーは常に具体名で扱うのが Yulang のスタイルです。
エラーを result 値に閉じる
my path = std::text::path::of_bytes (std::text::str::to_bytes "/tmp/data")
my wrapped = io_err::wrap: read_text path
case wrapped:
result::ok text -> text
result::err err -> err.showE::wrap は thunk を走らせ、対応するエラー effect を捕まえて result 値を返します。 err 側は自動生成された impl を通じて Display できます。
複数のエラーを一つにまとめる
pub error app_err:
file from io_err
parse from parse_err
my read_and_parse path =
app_err::up:
my text = read_text path
parse_json textfrom は広いエラー型に narrower エラーを取り込みます。 app_err::up は block 内部の narrower エラーを app_err に持ち上げる handler です。
自前 effect を handler で受ける
pub act log:
pub put: str -> ()
my run_into_strings(action: [log; 'e] 'a): ['e] ('a, list str) =
my $entries = []
my result = catch action:
log::put msg, k ->
&entries = $entries + [msg]
k ()
v -> v
(result, $entries)catch は捕まえた operation を row から除きます。 各 arm は payload と continuation k を受け取ります。 k value を呼ぶと、捕まえた地点以降の計算が再開します。
非決定的に探索する
use std::control::nondet::*
(each [1, 2, 3] + each [10, 20]).listeach xs は xs から 1 つ要素を非決定的に選びます。 .list は計算を走らせ、すべての結果を集めます。 .once / .logic などで結果の形を切り替えられます。
effectful な真偽値条件を扱う
if all [1, 2, 3] < any [2, 3, 4]:
"found at least one"
else:
"no overlap"Yulang の if は std::control::junction 経由で effectful な boolean 条件を受け取れます。 all xs / any xs で「全部」「いずれか」を表現します。
二つの型の間で cast を定義する
struct user_id { raw: int }
cast(x: user_id): int = x.raw
cast(x: int): user_id = user_id { raw: x }
my id: user_id = 7
my raw: int = idcast(x: A): B = body は A から B への変換規則を登録します。 compiler は注釈や引数のような expected-type 境界で、その規則の application を行います。
もう少し大きいサンプル
ここまでのレシピを組み合わせると、こういう小さなプログラムが書けます。 すべて Playground で動きます。
小さな式評価器
enum と再帰関数だけで電卓を書けます。
enum expr =
num int
| add (expr, expr)
| mul (expr, expr)
my eval e =
case e:
expr::num n -> n
expr::add (a, b) -> eval a + eval b
expr::mul (a, b) -> eval a * eval b
eval (expr::add (expr::num 2, expr::mul (expr::num 3, expr::num 4)))
// → 14(expr, expr) のように tuple を payload にするのが、複数引数 variant を表す素直な書き方です。
再帰で list を絞る
list spread pattern と再帰で、filter 的な処理を素直に書けます。
my is_even n =
std::int::eq (std::int::sub n (std::int::mul (std::int::div n 2) 2)) 0
my keep_evens xs =
case xs:
[] -> []
[x, ..rest] ->
my r = keep_evens rest
if is_even x:
[x] + r
else:
r
keep_evens [1, 2, 3, 4, 5, 6, 7, 8]
// → [2, 4, 6, 8]ループで集計する
my $x = ... の local reference は、ループでの集計と相性がいいです。
{
my $best = 0
for y in [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]:
if y > $best: &best = y
$best
}
// → 9$best は block の外には漏れません。 「ループの間だけ値を持ち回す」ような使い方が型に乗ります。
非決定的に探す
each と guard で、「条件を満たす組」を探索できます。
use std::control::nondet::*
{
my a = each 1..20
my b = each a..20
my c = each b..20
guard: a * a + b * b == c * c
(a, b, c)
}.list
// → [(3, 4, 5), (5, 12, 13), (6, 8, 10), (8, 15, 17), ...].list を .once に変えると、最初に見つかった 1 組だけが返ります。 前の選択を後の each の範囲に渡すことで、探索を早く絞れます。