標準愚痴出力

個人的なIT作業ログです。もしかしたら一般的に参考になることが書いているかもしれません(弱気

gmnlisp で、ユーザ関数を定義する

Go言語側で、割り算関数を定義する。gmnlisp においての割り算には整数割り算の (div) と、浮動小数点型実数による割り算の(quotient) が存在する。今回は (div) のオリジナル版を作成する。

割り算を取り上げたのは、割り算ではゼロ除算というエラーケースがあるためだ。このエラーはGo言語のエラー処理でも、ISLisp のエラー処理のどちらでも扱うことができる。

package main

import (
    "context"
    "errors"
    "fmt"

    "github.com/hymkor/gmnlisp"
)

// 独自に作ったゼロ割り算エラー
var errDivZero = errors.New("division by zero")

// 整数の割り算を行う関数。標準にも同じ関数があるが、Go と ISLisp では、
// マイナスの時の結果が微妙に違うので、一応、意味がある
func divAndMod(ctx context.Context, w *gmnlisp.World, first, second gmnlisp.Node) (gmnlisp.Node, error) {
    firstValue, err := gmnlisp.ExpectClass[gmnlisp.Integer](ctx, w, first)
    if err != nil {
        return nil, err
    }
    secondValue, err := gmnlisp.ExpectClass[gmnlisp.Integer](ctx, w, second)
    if err != nil {
        return nil, err
    }
    if secondValue == 0.0 {
        return nil, errDivZero
    }
    return &gmnlisp.Cons{
        Car: firstValue / secondValue,
        Cdr: firstValue % secondValue,
    }, nil
}

func main() {
    // gmnlisp のインタプリタのインスタンスを作成する
    lisp := gmnlisp.New()

    // 局所変数を定義する。(let ((a 10) (b 3)) .. ) と等価
    // 変数は戻り値のインスタンス内でのみ有効となる
    lisp = lisp.Let(
        gmnlisp.Variables{
            gmnlisp.NewSymbol("a"): gmnlisp.Integer(10),
            gmnlisp.NewSymbol("b"): gmnlisp.Integer(3)})
    // NewSymbol はシンボル(IDをもった識別子)を定義する。
    // 同じ名前であれば常に一意のID(番号)がふられる

    // 局所関数を定義する (labels ((div-and-mod (first second) ... )) ) と等価
    // 関数は戻り値のインスタンス内でのみ有効となる
    lisp = lisp.Flet(
        gmnlisp.Functions{
            gmnlisp.NewSymbol("div-and-mod"): gmnlisp.Function2(divAndMod)})
    // Function2 は引数二つの関数をオブジェクト化する
    // 他に引数1個のFunction1, 任意引数数の &Function{}
    // マクロや特殊関数用の SpecialF などもある

    ctx := context.Background()

    // 実行させる Lisp コードのリスト
    cases := []string{
        // 変数 a の内容を表示
        `(format (standard-output) "A=~S~%" a)`,
        // 変数 b の内容を表示
        `(format (standard-output) "B=~S~%" b)`,
        // Goで定義した関数を呼び出す
        "(div-and-mod a b)",
        // 割る数を 0 にして、わざとエラーをひきおこさせる
        "(setq b 0) (div-and-mod a b)",
        // Go関数で投げたエラーを Lisp 側の構文でとらえることができる
        // ( エラーオブジェクトが Node interface を実装していたら、
        //   Condition として細かく吟味することも可能だが、ここでは省略)
        `(catch 'fail
 (with-handler
  (lambda (c) (throw 'fail "Lisp program can catch errors"))
   (div-and-mod a b)))`,
    }

    for _, case1 := range cases {
        fmt.Printf("%s\n", case1)
        rv, err := lisp.Interpret(ctx, case1)
        if err != nil {
            fmt.Printf("--> FAIL: %s\n", err.Error())
            // エラーはスタックトレース情報でラッピングされているが、
            // errors.Is や errors.As でエラーを照合できる
            if errors.Is(err, errDivZero) {
                fmt.Println("--> Go program can catch errors\n")
            }
            fmt.Println()
        } else {
            fmt.Printf("--> PASS: %#v\n\n", rv)
        }
    }
}

実行結果:

> ./gmnlisp-sample.exe
(format (standard-output) "A=~S~%" a)
A=10
--> PASS: gmnlisp.nullType{}

(format (standard-output) "B=~S~%" b)
B=3
--> PASS: gmnlisp.nullType{}

(div-and-mod a b)
--> PASS: (3 . 1)

(setq b 0) (div-and-mod a b)
--> FAIL: division by zero
        at (DIV-AND-MOD A B)
--> Go program can catch errors


(catch 'fail
 (with-handler
  (lambda (c) (throw 'fail "Lisp program can catch errors"))
   (div-and-mod a b)))
--> PASS: "Lisp program can catch errors"