Racket
RacketはSchemeから派生したLisp系のプログラミング言語です。
Mr.EDを使ってGUI開発をやってみたかったので、racketの環境構築から言語仕様のピックアップまでまとめました。
なお、今回の記事では Mr.ED は触れていません。
環境
以下の環境でインストール、及び動作確認を行いました。
- OS :
Mac M1 Sonoma 14.7
- racket :
v8.15 [cs]
インストール
brew install racket
以下のコマンドで正常にインストールされていることを確認する
racket -v
開発環境
関連パッケージのインストール
インストールは raco
コマンドで行う
フォーマッタのイントール
※ 現在のユーザにのみインストールする場合は --scope user
に変更すること
raco pkg install --auto --scope installation fmt
ランゲージサーバのインストール
raco pkg install --auto --scope installation racket-langserver
vim
以下のプラグインを追加する
- 'wlangstroth/vim-racket'
以下のファイルを hello.rkt
として作成する
#lang racket
(println "Hello, World")
racket hello.rkt
を実行すると Hello, World
と出力される
VSCode
以下の拡張機能をインストールする
VSCodeのの設定(settings.json
)に以下を追記する
"[racket]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "evzen-wybitul.magic-racket"
}
使い方
- Alt + Enter : 選択位置(現在の行)をREPLで実行する
- F2 : シンボル名の一括変換
- Shift + F12 : シンボルの利用箇所
- Alt + Ctrl + N : ファイルの実行
- Alt + Shift + F : フォーマット
※ racketのソースファイルで右クリック(or Shift + F10)すると、いくつかのコマンドは実行できます。
言語仕様
公式のドキュメント + ChatGPTを参考にして、必要そうな言語仕様や関数などを、以下にまとめていきます。
関数の定義やforループなどの細かい文法については、省略しているので別途入門記事を参照してください。
※ 検索すると大学の講義資料などは出てくるのですが、Racket単体で簡潔な入門記事があまりないので、派生元のSchemaの入門記事等も参考にすると良いかもしれません。
- もうひとつの Scheme 入門
Booelan
; boolean? でブール型判定
(println (boolean? #t)) ; #t
; #t/#f = true/false
(println (if #t 1 0)) ; 1
(println (if #f 1 0)) ; 0
Numbers
分数を扱えるので、小数として計算させたい場合は気をつける。
; 分数
(println (/ 3 4)) ; 3/4
; 小数
(println (/ 3 4.0)) ; 0.75
; 浮動小数
(println 5#e-3) ; 0.05
; 複素数
(println 1+2i) ; 1+2i
Character
Character単体として扱う場合には #\
をつける必要がある
; integer to char
(println (integer->char 65)) ; #\A
; char to integer
(println (char->integer #\A)) ; 65
; display char
(displayln #\A) ; A
; lower
(println (char-downcase #\A)) ; #\a
; upper
(println (char-upcase #\a)) ; #\A
; compare
(char=? #\a #\A) ; #f
(char-ci=? #\a #\A) ; #t
String
文字列を扱うための組み込み関数は色々存在する。詳しくは以下のページを参照
Racketの文字列はMutable/Immutableの概念があることに注意する。
- 基本的に "apple" のように定義した文字列はImmutable(変更不可)
- Mutable(変更可能)な文字列にするためには以下のどちらかの方法を採る
-
make-string
による文字列定義 - Immutableな文字列を
string-copy
でMutableにする
-
(println (string-ref "Apple" 0)) ; #\A
; make-stringで作成した文字列は変更可能
(define s (make-string 7 #\.))
(displayln s) ; .....
(string-set! s 0 #\\)
(string-set! s 1 #\()
(string-set! s 2 #\^)
(string-set! s 3 #\o)
(string-set! s 4 #\^)
(string-set! s 5 #\))
(string-set! s 6 #\/)
(displayln s) ; \(^o^)/
; 以下のように定義された文字列は変更不可
(define imutable "imutable")
; 変更可能な文字列は上記の make-string か string-copy を利用して作成する
(define s2 (string-copy s))
(string-set! s2 3 #\ω)
(displayln s2) ; \(^ω^)/
; 部分文字列
(displayln (substring "pocket monster" 0 6)) ; pocket
(displayln (string->list "ABBA")) ; (A B B A)
(displayln (list->string (list #\A #\B #\B #\A))) ; ABBA
Byte String
基本的にMutableなので、文字列の処理で変更がかかることを前提とするようなものは、こちらを利用した方が良いかもしれない。
Symbol
SymbolはImmutable(変更不可)な文字列のように扱える仕組み。
racket自体が内部テーブルを持っており、同一名のSymbolはプログラム内でUniqueに扱われる。
Keyword
名前付き引数は以下のように書く
(define (hello #:name name)
(string-append "Hello, " name "!"))
(displayln (hello #:name "Bohm")) ; Hello, Bohm
リストとペア
リストはペアを再帰的に適用したものなので、大きな違いは無いが、以下の点は異なるので注意する。
- リストの終端要素は空リスト
()
- ペアは何でも良い
-
display
などで出力した際にペアは(1 . 2)
のようにドット表記される- リストはドットなしで表示
ペアの例
(let ([p (cons 1 (cons 2 3))])
(displayln p) ; (1 2 . 3))
(displayln (car p)) ; 1
(displayln (cdr p)) ; (2 . 3)
)
リストの例
(let ([ls '(1 2 3)])
(displayln (car ls)) ; 1
(displayln (cdr ls)) ; (2 3)
(displayln (cdr (cdr (cdr ls)))) ; ()
)
末尾要素が空リストかどうかで、ドット表記があるかどうかが変わる
(displayln (cons 2 1)) ; (2 . 1)
(displayln (cons 2 (cons 1 '()))) ; (2 1)
Vector
ペアやリストは固定長で線形探索なので、以下のケースに該当する場合はVectorを利用するとよい。
- 要素数が可変長
- ランダムアクセスを行う
- 部分的に変更する
(let [(vs (vector 1 2 3))]
(displayln vs) ; #(1 2 3)
(displayln (vector-ref vs 1)) ; 2
; 要素の変更
(vector-set! vs 1 10)
(displayln vs) ; #(1 10 3)
; 部分ベクトル
(displayln (vector-copy vs 2 3)) ; #(3)
)
Stencil Vector
mask値を指定してベクトルを管理できる。トライ木など利用できる。
うまく使うと通常のベクトルよりも高速らしい。
定義する際にはマスク値の1ビットの数とリストの要素数が一致している必要がある。
以下の場合だと、#b111
に変更しても問題ないが、#b1100
などに変更するとエラーが発生する。
ベクトルの要素を変更する場合、破壊的変更で良ければ stencil-vector-set!
を利用し、
元のベクトルを残す場合は stencil-vector-update
を利用する。
以下にサンプルを書いたが、クセが強いので、高速化を行いたい場合を除いて、積極的に使うのは避けたほうが良いかもしれない。
(let [(vs (stencil-vector #b1101 1 2 3))]
(displayln vs) ; #<stencil 13: 1 2 3>
; 各要素へのアクセス
(displayln (stencil-vector-ref vs 0)) ; 1
(displayln (stencil-vector-ref vs 1)) ; 2
(displayln (stencil-vector-ref vs 2)) ; 3
; 破壊的変更
(stencil-vector-set! vs 0 100)
(displayln vs) ; #<stencil 13: 100 2 3>
; 追加する場合は remove-mask を #b0 にしておく
(displayln (stencil-vector-update vs #b0 #b10 999)) ; #<stencil 15: 100 999 2 3>
; 変更する場合は remove-mask と add-mask の値を一致させておく
(displayln (stencil-vector-update vs #b1 #b1 999)) ; #<stencil 13: 999 2 3>
; 削除する場合は add-mask の値を #b0 にしておく
(displayln (stencil-vector-update vs #b1 #b0)) ; #<stencil 12: 2 3>
)
クォート付きリスト (リストリテラル)
式にクォートを付与すると、その式を評価せずに単なるデータとして扱うことができる。
(+ 1 2 3)
を実行すると通常は 6
と評価されてしまうが、'(+ 1 2 3)
と書くと6
ではなく (+ 1 2 3)
そのものとして扱える。
リストリテラルの性質を利用すると、遅延して評価することができるが
Racketのevalはデフォルトでは現在の名前空間を考慮しないため、以下のコードを実行するとエラーになる。
(println (eval '(+ 1 2 3)))
実際に発生するエラーは以下の通り
+: unbound identifier;
also, no #%app syntax transformer is bound
at: +
in: (+ 1 2 3)
context...:
回避するためには、以下のような標準ライブラリを含むベースの名前空間を設定する必要がある。
(parameterize ([current-namespace (make-base-namespace)])
(println (eval '(+ 1 2 3))))
コード量が無駄に増えるので、可能であれば以下のようにリテラルリストに演算子を適用する形で書くと良い
(println (apply + '(1 2 3))) ; 6
Box
値を共有するための仕組み。
shared-box
をグローバルに定義して、各関数で値を共有する例
(define shared-box (box 0))
(define (increment!) (set-box! shared-box (add1 (unbox shared-box))))
(define (decrement!) (set-box! shared-box (sub1 (unbox shared-box))))
(displayln (unbox shared-box)) ; 0
(increment!)
(displayln (unbox shared-box)) ; 1
(decrement!)
(displayln (unbox shared-box)) ; 0
グローバルにboxを持たずに、状態を持つ関数を作成する例
(define (make-counter)
(let ([count (box 0)])
(lambda ()
(set-box! count (add1 (unbox count)))
(unbox count))))
(define counter1 (make-counter))
(define counter2 (make-counter))
(displayln (counter1)) ; 1
(displayln (counter1)) ; 2
(displayln (counter2)) ; 1
(displayln (counter2)) ; 2
Sequence
順序付きの値の集合を定義する。
細かく制御できる反面記述量が多い。
(define (powers i)
(make-do-sequence (lambda ()
; 値 (シーケンスの位置を値に変換する関数を書く)
(values (lambda (pos) (expt (+ i pos) 2))
; 次の位置を決定する関数 (add1 なので +1)
add1
; 初期位置
0
; continue-with-pos?
; 現在の位置を基準に、終了するかどうかを決定する関数
#f
; continue-with-val?
; 建材の値を基準に、終了するかどうかを決定する関数
#f
; continue-after-pos+val?
; 現在の位置と値を基準に、終了するかどうかを決定する関数
; ※ 現在の値を含む
#f))))
; 利用例
(for/list ([x (powers 0)]
[_ (in-range 10)])
x)
(displayln (for/list ([x (powers 0)]
[_ (in-range 10)]) ; (in-range 10)で先頭10個の要素に制限
x))) ; (0 1 4 9 16 25 36 49 64 81)
Stream
Sequenceと似たような動作をするが、こちらは遅延評価に特化している感じである。
Sequenceで作成した2乗のリストは、Streamだとかなり簡単に書ける。
(define s (for/stream ([i (in-naturals)]) (expt i 2)))
(displayln (stream-ref s 5)) ; 25
(displayln (stream->list (stream-take s 10))) ; (0 1 4 9 16 25 36 49 64 81)
Generator
こちらも無限リストの生成などに利用できる。
他言語(Pythonなど)でも採用されているGeneratorを利用するためのモジュール。
(require racket/generator) ; デフォルトでロードされていないのでrequireが必要
(define (power-generator n)
(generator ()
(for ([i (in-range 0 n)])
(yield (expt i 2)))))
(define power10 (power-generator 10))
(displayln (for/list [(_ (in-range 10))] (power10))) ; (0 1 4 9 16 25 36 49 64 81)
Set
重複を許さない要素の集合
(displayln (set 1 1 2 2 3)) ; #<set: 1 2 3>
Procedures
Procedures のぺージに存在する利用頻度の高そうな関数をピックアップしておく
; 関数適用
(displayln (apply + '(1 2 3))) ; 6
; 関数合成(複数引数も可)
; x => x^2 + 10
(define square-plus10 (compose (lambda (x) (+ x 10)) (lambda (x) (expt x 2))))
(displayln (square-plus10 2)) ; 14
; x, y => (x + y)^2
(define plus-square (compose (lambda (x) (expt x 2)) +))
(displayln (plus-square 2 3)) ; 25
; 関数合成(単一引数のみ)
; compose を使っても同様に書ける
; x => - x^2
(define square-minus (compose1 - (lambda (x) (expt x 2))))
; x => (-x)^2
(define minus-square (compose1 (lambda (x) (expt x 2)) -))
(displayln (square-minus 10)) ; -100
(displayln (minus-square 10)) ; 100