Skip to content
Playground

クックブック

日常的な Yulang のタスクを目的別に引けるレシピ集です。 多くの節では定型的な書き方に短い説明を添え、設定ファイル、ファイル編集、log 集計、もう少し大きいサンプルでは長めの例を扱います。 背景の詳細は言語リファレンスへリンクしています。

例では、C 風の呼び出しより、Yulang らしい 括弧を減らした書き方 を中心に使います。 f x y の bare application、f: ... の colon application、字下げ block です。

値と関数を定義する

yulang
my answer = 42
my greet name = "hello, " + name
greet "Yulang"

my は private な binding です。 名前のあとに置いた pattern が curry された引数になります。

関数

関数を公開する

yulang
pub double x = x + x

pub は外部へ export します。 our は囲んでいる companion module 内で見えるようにします(method 定義での既定)。

module

case で場合分けする

yulang
my describe n =
    case n:
        0 -> "zero"
        x if x < 0 -> "negative"
        _ -> "positive"

arm は上から順に試されます。 guard は pattern の後ろに if を続けて書きます。

パターンマッチ

struct に method をつける

yulang
struct point { x: int, y: int } with:
    our p.norm2 = p.x * p.x + p.y * p.y

point { x: 3, y: 4 } .norm2

with: は struct の companion module を開きます。 receiver 名 p.norm2 を呼んだときの値を指します。

struct と role

role を型に対して実装する

yulang
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 シグネチャと対応します。

struct と role

ループから早期脱出する

yulang
sub:
    for x in 0..:
        if x == 5: return x
    0

sub: は early-return の scope を開きます。 return value は最も近い sub: を抜けます。 for の中では last / next / redo でループそのものを制御します。

制御構文

$x / &x で局所的な可変参照を使う

yulang
{
    my $count = 0
    &count = $count + 1
    &count = $count + 1
    $count
}

my $x = … は局所の mutable binding を作ります。 $x で読み、&x = value で書きます。 内部的には小さな handled var effect として展開されるので、effect 系の外に逃げず、ちゃんと型に乗ります。

text ファイルを読む

yulang
my text = read_text "data.txt"
text.say

strpath に widen されるので、通常の path なら文字列リテラルをそのまま渡せます。 filesystem エラーは effect row の io_err として投げられます。 呼び出し側に値としての result が必要な境界だけ wrap します。

yulang
my wrapped = io_err::wrap: read_text "data.txt"
case wrapped:
    result::ok text -> text
    result::err _ -> ""

std::io::file

設定ファイルを読む

設定ファイル、ファイル編集、log 集計の例はexamples/config-file-text/ にあります。

次は一部省略の例です。 設定ファイルを method-form の文字列 API で読み、= の左右を trim してからport を数値化します。

yulang
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 は汚れません。

yulang
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 に変換します。

yulang
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 です。

オプショナル引数を作る

yulang
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 を参照できます。

関数

型付きエラーを投げて捕まえる

yulang
my path = std::text::path::of_bytes (std::text::str::to_bytes "/tmp/data")

catch read_text path:
    io_err::not_found _, _ -> "(missing)"
    value -> value

fail ee.throw の薄皮です。 catch arm は operation 名を直接書いてエラーを捕まえます。 エラーは常に具体名で扱うのが Yulang のスタイルです。

エラー

エラーを result 値に閉じる

yulang
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.show

E::wrap は thunk を走らせ、対応するエラー effect を捕まえて result 値を返します。 err 側は自動生成された impl を通じて Display できます。

エラー → wrap

複数のエラーを一つにまとめる

yulang
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 text

from は広いエラー型に narrower エラーを取り込みます。 app_err::up は block 内部の narrower エラーを app_err に持ち上げる handler です。

エラー → from 集約

自前 effect を handler で受ける

yulang
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 を呼ぶと、捕まえた地点以降の計算が再開します。

effect

非決定的に探索する

yulang
use std::control::nondet::*

(each [1, 2, 3] + each [10, 20]).list

each xsxs から 1 つ要素を非決定的に選びます。 .list は計算を走らせ、すべての結果を集めます。 .once / .logic などで結果の形を切り替えられます。

std::control::nondet

effectful な真偽値条件を扱う

yulang
if all [1, 2, 3] < any [2, 3, 4]:
    "found at least one"
else:
    "no overlap"

Yulang の ifstd::control::junction 経由で effectful な boolean 条件を受け取れます。 all xs / any xs で「全部」「いずれか」を表現します。

二つの型の間で cast を定義する

yulang
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 = id

cast(x: A): B = bodyA から B への変換規則を登録します。 compiler は注釈や引数のような expected-type 境界で、その規則の application を行います。

cast

もう少し大きいサンプル

ここまでのレシピを組み合わせると、こういう小さなプログラムが書けます。 すべて Playground で動きます。

小さな式評価器

enum と再帰関数だけで電卓を書けます。

yulang
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 的な処理を素直に書けます。

yulang
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 は、ループでの集計と相性がいいです。

yulang
{
    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 の外には漏れません。 「ループの間だけ値を持ち回す」ような使い方が型に乗ります。

非決定的に探す

eachguard で、「条件を満たす組」を探索できます。

yulang
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 の範囲に渡すことで、探索を早く絞れます。

関連ページ

Yulang