読者です 読者をやめる 読者になる 読者になる

きくらげ観察日記

好きなことを、適当に。

Scheme始めました

Gauche

某友人の影響でLispに目覚めてしまったので、とりあえずGaucheの勉強を始めることにしました。
(とは言っても授業でコンパイラ作ったりCPU作ったりしてて結構忙しいので、Gauche自体の勉強はかなりゆっくりになると思いますが)

なぜ数あるLisp方言やScheme方言の中からGaucheを選んだかと言うと、Lisp方言の中では比較的モダンな設計のSchemeの中でも、Gaucheが一番Common Lisp的な黒魔術的処理も書きやすそうだったからです。(他のLisp方言は全くと言っていいほど触ったことがないので、間違ってたらごめんなさい)


とりあえず手始めに、Gaucheモナドを書いてみました。

(define (create-monad :key return >>=)
  (cons return >>=))

(define (get-return x) (car x))
(define (get->>= x) (cdr x))

(define-macro (with-monad monad . body)
  (define current-monad (gensym))
  (define (create-monad-body rest-body)
    (define (head) (car rest-body))
    (define (tail) (cdr rest-body))
    (if (null? rest-body)
        '()
        (begin
          (cond
           ((and (list? (head)) (equal? '<- (cadr (head))))
            `(((get->>= ,current-monad)
              ,(caddr (head))
              (lambda (,(car (head))) ,@(create-monad-body (tail))))))
           ((and (list? (head)) (equal? 'return (car (head))))
            (cons
             `((get-return ,current-monad) ,(cadr (head)))
             (create-monad-bondy (tail))))
           (else (cons (head) (create-monad-body (tail))))))))
  `((lambda ()
      (define ,current-monad ,monad)
      ,@(create-monad-body body))))

Haskellのdo記法(Scalaのforのほうが近いかも)らしき文法をGaucheに追加するマクロです。

Template HaskellやQuasi Quotesのような特殊な機能を使わなくても、簡単にプログラムの構文木そのものを弄ることができます。

これがLisp最大の強みですね。ハマる人が多いのもなんとなく分かります。

上で作ったモナドは以下のようにして利用します。例えばHaskellで言うMaybeモナドがほしい場合、とりあえず以下のように自前でMaybeを定義します。

(define (just x) (cons 'just x))
(define nothing (cons 'nothing #f))

(define (just? x) (symbol=? 'just (car x)))
(define (nothing? x) (symbol=? 'nothing (car x)))
(define (maybe-value x) (cdr x))

そして以下のように、MaybeのMonadに対する「インスタンス宣言」を行います

(define (maybe-return x) (just x))
(define (maybe->>= x f)
  (if (nothing? x)
      nothing
      (f (maybe-value x))))

(define maybe-monad
  (create-monad
   :return maybe-return
   :>>= maybe->>=))

実際にモナドを使用するには、上で定義したwith-monad構文を使用します。具体的には以下のような感じで使用します。

;; 分母が0のときはnothingを返す除算
(define (safe-/ x y)
  (if (= y 0)
      nothing
      (just (/ x y))))

;; 使用例
;; with-monadがHaskellのdo記法に相当
;; <-とreturnはそれぞれHaskellの<-とreturnとだいたい同じ
(define (div-by-y-and-z x y z)
  (with-monad maybe-monad
    (a <- (safe-/ x y))
    (b <- (safe-/ x z))
    (return (+ a b))))

実行結果は以下のようになります。

gosh> (div-by-y-and-z 2 3 4)
(just . 7/6)
gosh> (div-by-y-and-z 2 0 1) ; nothingになったところで計算が止まる
(nothing . #f)
gosh> (div-by-y-and-z 2 1 0)
(nothing . #f)

実際にどんな処理を行っているかというと、だいたいHaskellのdo記法と同じで

(x <- (monadic-action))
...

の形の式を

(hoge->>= (monadic-action) (lambda (x) ...))

に書き換えてるだけです。


他にも、リストモナドとかStateモナドも以下のようにして定義することができます。

;; List
(define (list-return x) (list x))
(define (list->>= x f) (concatenate (map f x)))

(define list-monad
  (create-monad
   :return list-return
   :>>= list->>=))

;; State
(define (state-return a) (lambda (s) (cons a s)))
(define (state->>= x f)
  (lambda (s)
    (define x-result (x s))
    (define a2 (car x-result))
    (define s2 (cdr x-result))
    ((f a2) s2)))

(define (get) (lambda (s) (cons s s)))
(define (put x) (lambda (_) (cons '() x)))

(define state-monad
  (create-monad
   :return state-return
   :>>= state->>=))

;; 使用例
(define (list-example)
  (with-monad list-monad
    (a <- (list 1 2 3))
    (b <- (list 4 5 6))
    (return (cons a b))))

(define (state-example)
  (with-monad state-monad
    (state <- (get))
    (_ <- (put (+ state 1)))
    (return (* state 2))))

それぞれ実行結果は以下のようになります。

gosh> (list-example)
((1 . 4) (1 . 5) (1 . 6) (2 . 4) (2 . 5) (2 . 6) (3 . 4) (3 . 5) (3 . 6))
gosh> ((state-example) 5)
(10 . 6) ; result => (* 5 2), state => (+ 5 1)

ちなみにGauche歴というかLisp歴自体触り始めてから12時間も経ってないので、もっといい書き方とかある場合は教えてください。

あと、Lispの場合は継続使ったほうが綺麗にモナド書けそうな気もするんで、気が向いたらやってみたいです。