kb84tkhrのブログ

何を書こうか考え中です あ、あと組織とは関係ないってやつです 個人的なやつ

RacketのMacroを調べてみる (8)

次の例です
ちょっとエラー処理に凝ったりしてます

何をするかは説明するより見たほうが速いかな

> (define js (hasheq 'a (hasheq 'b (hasheq 'c "value"))))
> (hash.refs js.a.b.c)
"value"

hasheqというのはいわゆるハッシュテーブルを作る関数で、
ここではネストしたハッシュテーブルを作ってjsという名前を付けています
ネストしたハッシュテーブルから指定した要素の値を持ってくるわけですが
指定の仕方がRacketていうかlispぽくないですね?
ドット区切りになってます
ここをマクロで作ります

ちなみに生のRacketで書くと
(hash-ref (hash-ref (hash-ref js 'a) 'b) 'c)となります

最初にヘルパー関数を定義します
こんな風に使えるもの

> (hash-refs js '(a b c))
"value"

まあこれだけでも十分便利なんですが話が終わってしまうので

定義はこう

(define/contract (hash-refs h ks [def #f])
  ((hash? (listof any/c)) (any/c) . ->* . any)
  (with-handlers ([exn:fail? (const (cond [(procedure? def) (def)]
                                          [else def]))])
    (for/fold ([h h])
              ([k (in-list ks)])
      (hash-ref h k))))

関数定義に契約がついてますね
契約はRacketのひとつの大きな売りのようですが実はまだよく知りません
そのうち勉強したいと思います
ていうかRacketってなにげにてんこ盛りな言語なんですよ
Schemeはシンプルな言語っていうイメージがありますが正反対

マクロ本体
細切れにしてコメントを付けていきます

(define-syntax (hash.refs stx)
  (syntax-case stx ()
    [(_)
     (raise-syntax-error #f "Expected hash.key0[.key1 ...] [default]" stx #'chain)]

引数がなにも無ければエラー

    [(_ chain)
     #'(hash.refs chain #f)]

ふたつめの引数には、指定した名前の要素が見つからなかったときに
返す値を書くことができます
省略された場合は#fが指定されたふりをしてもういちどマクロを展開します

    [(_ chain default)

名前とデフォルト値が両方指定されたとき
ここが本体です

     (unless (identifier? #'chain)
       (raise-syntax-error #f "Expected hash.key0[.key1 ...] [default]" stx #'chain))

これはfender
名前が識別子でないときはエラーにします

     (let* ([chain-str (symbol->string (syntax->datum #'chain))]

まずはドットで区切られた識別子のsyntaxをただの文字列に変換します
上の例で言うと#'js.a.b.cをまずjs.a.b.cにして、さらに"js.a.b.c"にしています

            [ids (for/list ([str (in-list (regexp-split #rx"\\." chain-str))])
                   (format-id #'chain "~a" str))])

文字列をドットのところで分割してできたひとつひとつの文字列を
同じ名前の識別子に戻します
"js.a.b.c"から(#'js #'a #'b #'c)を作ってることになるかな

       (unless (and (>= (length ids) 2)
                    (not (eq? (syntax-e (cadr ids)) '||)))
         (raise-syntax-error #f "Expected hash.key" stx #'chain))

またエラー処理
短すぎたらエラーにします
|は見慣れませんがシンボル用の引用符
||は名前のないシンボルってことになるのかなあ

       (with-syntax ([hash-table (car ids)]
                     [keys (cdr ids)])
         #'(hash-refs hash-table 'keys default)))]))

ここが本当の本体ですが(#'js #'a #'b #'c)まで作ってあるので
大したことはしてませんね
#'js(#'a #'b #'c)に分割して、(hash-refs js '(a b c) #f)
作ってます

エラーメッセージが親切になったのはいいんですが
本体のロジックとエラー処理が混じってしまって見にくいですね
syntax-parseはそのあたりをうまくやってくれるとのことです
期待