Docsの目次

コードを書きながら学ぶ

目次 · 前:準備と最初の実行 · 調べる:文法の早見表

Pythonで関数やリストを書いたことがある人向けに、同じ目的を現在のNagiでどう書くかを試します。High(.nagi)は字下げで書く構文ですが、値を渡すときや失敗を扱うときにはPythonとの違いがあります。Nagiをインストールし、作業用のフォルダーでnagic run ファイル名.nagiを実行してください。

このページの目次

1. 値と型

数を増やして表示したいとき、Pythonでは次のように書けます。

count = 10
count += 1
print(count)

Nagiでも代入から始められます。次の断片はmainなどの関数の中に書きます。

count = 10            # i64。型は自動で決まる
rate = 1.5            # f64
enabled = True       # bool。True / Falseと書く
name = "Nagi"        # str。日本語も書ける
age: i32 = 18        # 型を指定するなら「名前: 型 = 値」
count += 1
print(count)

この断片は11を表示します。変数は再代入できますが、型を変更することはできません。count = "十"は型エラーです。条件にはboolを使い、if count:のように整数を真偽値として扱うことはできません。

異なる整数型は自動で混ざりません。i64(age)でi32をi64へ広げられます。逆のi32(count)は範囲外の可能性があるのでResult[i32, Error]を返します。失敗の扱いは後で説明します。

一言でいうと、代入から型が決まり、同じ型で更新します。型の一覧と変数の書式で詳しく調べられます。

2. 関数を定義する

足し算を何度も使いたいなら、関数にします。Pythonでは引数の型を省略できます。

def add(a, b):
    return a + b

print(add(20, 22))

Nagiでは引数と、返す値の型を書き、実行する処理をmainに置きます。

def add(a: i64, b: i64) -> i64:
    return a + b

def main():
    print(add(20, 22))

このコードを保存して実行すると42と表示します。引数は名前: 型、戻り値は-> 型です。戻り値を省略した関数はunit(値を返さない)です。mainがプログラムの入口になります。

Pythonのようにファイル末尾へprint(add(...))を直接書くと、Nagiでは受理されません。上の例のようにmainへ入れます。一言でいうと、型付きの関数を定義し、mainから呼びます。関数とreturnで制約を確認できます。

3. 配列、class、分岐、繰り返し

いくつかの数をまとめて合計し、結果で処理を分けたいとします。Pythonのリスト追加や反復は次のように書けます。

values = [1, 2, 3]
values.append(4)
for value in values:
    print(value)

Nagiの配列は[1, 2, 3]、型注釈はList[i64]です。append(values, 4)で末尾に追加します。classは名前付きフィールドをまとめる型です。

次のコードをbasics.nagiとして保存してください。サンプルにも同じコードがあります。

class Point:
    x: f64
    y: f64

def sum_numbers(values: view[i64]) -> i64:
    total = 0
    for value in values:
        total += value
    return total

def main():
    values = [1, 2, 3]
    append(values, 4)
    total = sum_numbers(view(values))
    print(total)

    point = Point(x=3.0, y=4.0)
    print(point.x + point.y)

    if total >= 10 and len(values) == 4:
        print("OK")
    else:
        print("NG")

    for index in range(3):
        print(index)

    count = 0
    while count < 2:
        count += 1
    print(count)
nagic run basics.nagi

プログラムの出力は順に10、7、OK、0、1、2、2です。

  • Point(x=..., y=...)は全フィールドを名前付きで指定します。Point(3.0, 4.0)とは書きません。
  • point.xでフィールドを読みます。現在はclassにmethodや継承を定義できません。
  • for value in valuesは要素を順に読みます。数値などの要素は値をコピーし、文字列やそれを持つclassは読み取り専用で借ります。暗黙にコピーできる性質をCopyと呼びます。借用中は元の配列を変更できません。所有権に詳しい例があります。
  • range(3)は0, 1, 2です。引数は1つで、終端を含みません。
  • whileは条件がTrueの間繰り返します。break / continueは未対応です。
  • and / or / notで条件を組み合わせます。elifはないため、必要ならelseの中にifを書きます。

values.append(4)はNagiでは使えません。append(values, 4)に直します。一言でいうと、配列を繰り返し読み、classで名前付きの値をまとめます。分岐とループと型で詳しく調べられます。

view(values)は「配列を渡して手放す」代わりに「読み取り用に借りる」書き方です。次で説明します。

4. 読むだけならviewで借りる

関数に文字列を読ませた後も、手元で使いたいとします。Pythonでは代入しただけで新しいリストは作られず、二つの名前が同じリストを指します。

a = [1, 2]
b = a
b.append(3)
print(a)  # [1, 2, 3]

Nagiでは、読むだけの関数はview[str]を受け取り、呼び出す側はview(name)で貸します。次は完全なコードで、borrowing.nagiとして実行できます。

def name_size(name: view[str]) -> i64:
    return len(name)

def main():
    name = "Nagi"
    print(name_size(view(name)))
    print(name_size(view(name)))
    duplicate = copy(view(name))
    print(duplicate)
    print(name)

出力は4、4、Nagi、Nagiです。copy(view(name))は別の所有文字列を作ります。元の文字列とは別の領域を使います。

文字列や配列を自作関数へ所有値として渡すと、その値を手放します。この受け渡しをmoveと呼びます。

別のローカル変数へ文字列を渡す場合は、次のように明示します。この代入規則はNagi 0.1.11で公開済みです。利用するcompilerが0.1.11以降であることを確認してください。

from std.ownership import move

def main():
    name = "Nagi"
    destination = move(name)
    print(destination)

出力はNagiです。destination = nameでは所有する非Copyローカルの通常代入として拒否され、move(name)で渡した後のnameも使えません。元の文字列も読むなら、moveした後にcopyを追加するのではなく、代入をdestination = copy(view(name))へ置き換えます。数値等のCopy値と新しい値を作る式は通常代入でき、関数への引数・return・field/indexの既存ルールは変わりません。importの別名と正確な範囲は代入と明示moveで確認できます。

次は意図的にcheckエラーになる完全な例です。

# エラーになる例
def take_name(name: str):
    print(name)

def main():
    name = "Nagi"
    take_name(name)
    print(name)       # 自作関数へmoveした後なので使えない

渡したnameは後で使えません。読むだけなら、最初の例のように引数をview[str]へ変えてview(name)を渡します。一言でいうと、読むだけならviewで貸します。

printやlenは入力を読み取る組み込み関数なので、それらに渡しただけでは文字列をmoveしません。すべての関数が同じ受け渡し方をするわけではありません。

len(str)は文字数ではなくUTF-8のbyte数です。len("あ")は3です。viewが生きている間の元データの変更にも制約があります。詳細は所有権にあります。

5. 失敗する処理はResultで返す

入力を数に変えたいとき、Pythonではint(text)が失敗すると例外を送出します。呼び出し元へ伝えるか、その場で扱うかを選べます。

try:
    number = int("abc")
except ValueError:
    number = 0

Nagiでは処理の失敗を返却値として扱います。値がないだけならT?、処理の成功か失敗を返すならResult、通常の入力不正などを返す用途ではない異常はpanicと区別します。T?の取り出し方は値がない場合を扱うで試せます。

Result[T, Error]は「成功ならT、失敗ならError」という戻り値です。ok(value)で成功、error("理由")で失敗を返します。

try 処理は、成功した値を取り出し、失敗したらその場で呼び出し元へ失敗を返します。そのため、tryを書く関数自身もResultを返す必要があります。

次は入力した整数を2倍にする完全なコードです。input.nagiとして保存してください。サンプルにもあります。

def double_nonnegative(text: view[str]) -> Result[i64, Error]:
    value = try parse_i64(text)
    if value < 0:
        return error("0以上の整数を入力してください")
    return ok(value * 2)

def main() -> Result[unit, Error]:
    write("整数を入力 > ")
    text = try read_line()
    doubled = try double_nonnegative(view(text))
    print(doubled)
    return ok(print("完了"))
nagic run input.nagi

21を入力してEnterを押すと、42と完了を表示します。abcは数値の変換に失敗し、-1は自分で書いたエラーになります。どちらもmainまで伝わり、プログラムは失敗で終了します。

Pythonのintと異なり、i64には上限があります。この例は小さな整数を想定し、2倍にする前の範囲確認は省いています。大きな入力では整数演算の規則も確認してください。

writeは改行なし、read_lineは1行入力です。return ok(print("完了"))は、表示処理が返すunitを成功として返しています。

NagiのtryはPythonのtry: ... except:とは別の構文です。その場で既定値に回復するなど、成功・失敗を分けたいときはmatchを使います。次は関数の例です。

def number_or(text: view[str], fallback: i64) -> i64:
    match parse_i64(text):
        case Ok(number):
            return number
        case Err(_):
            return fallback

この関数は"21"なら21、"abc"なら指定したfallbackを返します。ResultではOkとErrの両方を書きます。括弧内の名前はそのcase内で使い、不要な値は_にします。matchはnullableのSome/Noneと独自enumにも対応します。parse_i64(text)を呼ぶだけでResultを捨てるとcheckが拒否します。伝えるならtry、回復するなら上のmatchで処理します。一言でいうと、失敗も返却値として受け取って扱います。詳しくはエラー処理と型を参照してください。

6. ファイルを分ける

関数を別ファイルへ分けたいとき、Pythonならfrom math_helpers import addのようにmodule名で読み込みます。Nagiではファイルの相対パスを引用符で囲みます。同じディレクトリに2つのファイルを作ります。

math.nagi:

def add(a: i64, b: i64) -> i64:
    return a + b

imports.nagi(実行するファイル):

import "math.nagi"

def main():
    print(add(20, 22))
nagic run imports.nagi

完成ファイルを実行すると42です。importのパスは、実行したターミナルではなくimportを書いたファイルの場所を基準にします。読み込まれるmath.nagiにはmainは不要です。

この書き方では同じ名前空間へ読み込むため、add(...)で呼びます。import "math.nagi" as mathに変えるとmath.add(...)で呼べます。classや関数だけを選ぶfrom文も使えます。import mathだけではこのファイルを読み込めません。import "math.nagi"に直します。一言でいうと、import元からの相対パスで読み込みます。同名の定義を区別する方法やRustライブラリとの連携はimportとRust連携を参照してください。

7. asyncとAPIへ進む

タイマーやI/Oの結果を待ちたいとき、Pythonでもasync defの中でawaitします。

import asyncio

async def wait_once():
    await asyncio.sleep(0.01)  # 秒

Nagiもasync defとawaitを使います。次は完全なコードです。

async def main() -> Result[unit, Error]:
    await sleep(10)
    print("10ミリ秒待った")
    return ok(print("完了"))

10ミリ秒待った、完了の順で表示します。Pythonのasyncio.sleepは秒、Nagiのsleepはミリ秒です。sleepはunitを返すのでawaitだけ、失敗し得るDB関数などはtry await sqlite.open(path, config)のように書きます。awaitは非同期処理を待ち、tryは待った結果の失敗を伝えます。

sleep(10)だけでは待っていないためcheckが拒否します。await sleep(10)に直します。awaitしただけで別taskが作られるわけではありません。一言でいうと、awaitで結果を待ちます。複数の処理を進めるspawnと、その寿命を管理するscopeはasyncで試せます。

次は最初の小さなCLIアプリで、入力を検査し境界値を試してください。その後HTTPとHTMLでブラウザーから呼べるAPIを作れます。書式だけ調べたいときは文法の早見表、関数の引数を調べたいときは組み込み関数を使えます。

Nagi 0.2.0 · Docsこのページのソース