OCamlのlambda IRをいじる(再帰関数)
再帰関数のことを忘れていた!
というわけで幕間的な記事。
Factorial
再帰で階乗なコード:
let () = let rec f x = if x = 0 then 1 else x * (f (x-1)) in print_int @@ f 5
-dlambda
でコンパイル:
(seq (let (*match*/83 = (letrec (f/80 (function x/81[int] : int (if (== x/81 0) 1 (* x/81 (apply f/80 (- x/81 1)))))) (apply (field 43 (global Stdlib!)) (apply f/80 5)))) 0a) 0a)
再帰関数だとインライン化はできない(当然といえば当然)。それ以外では(let (... = ...) ...)
という構文が(letrec (... ...) ...)
になったくらいの違いだ。(なぜlet
で=
を使っていてletrec
では使っていないのかは少し謎だが・・・ =[int]
のようにboxing関連の型情報を伝える必要がないからか?)
(function ... )
の部分は本当にまったく非再帰的な関数と同じで、この部分だけ見ると再帰しているかどうかはわからない。
相互再帰的なodd/even
相互再帰も見ていく。
let () = let rec odd x = if x = 0 then false else even (x-1) and even x = if x = 0 then true else odd (x-1) in print_int @@ if odd 5 then 1 else 2
-dlambda
でコンパイル:
(seq (let (*match*/85 = (letrec (odd/80 (function x/82[int] (if (== x/82 0) 0a (apply even/81 (- x/82 1)))) even/81 (function x/83[int] (if (== x/83 0) 1a (apply odd/80 (- x/83 1))))) (apply (field 43 (global Stdlib!)) (if (apply odd/80 5) 1 2)))) 0a) 0a)
およそ期待どおりといったところか。(letrec (.. ..) ..)
の中のカッコに相互再帰な関数を入れていくだけ。
モジュールレベルでodd/even
相互再帰関数をローカルではなくモジュールのトップレベルで定義してみる:
let rec odd x = if x = 0 then false else even (x-1) and even x = if x = 0 then true else odd (x-1) let () = print_int @@ if odd 5 then 1 else 2
-dlambda
でコンパイル:
(seq (letrec (odd/80 (function x/82[int] (if (== x/82 0) 0a (apply even/81 (- x/82 1)))) even/81 (function x/83[int] (if (== x/83 0) 1a (apply odd/80 (- x/83 1))))) (seq (setfield_ptr(root-init) 0 (global Rec1!) odd/80) (setfield_ptr(root-init) 1 (global Rec1!) even/81))) (let (*match*/87 = (apply (field 43 (global Stdlib!)) (if (apply (field 0 (global Rec1!)) 5) 1 2))) 0a) 0a)
(letrec (X ... Y ...) (seq (setfield_ptr ... X) (setfield_ptr ... Y)))
という形にコンパイルされる。つまり相互再帰的な部分がすべて定義された後で、それらを一つずつモジュールに保存していく流れになっている。
次回
これでようやくデータ型に移れる。まずはタプルから。
OCamlのlambda IRをいじる(関数)
前回に続いて、ローカルとモジュールレベルでの関数定義がどうlambda IRにコンパイルされるかを見ていく。
ローカル関数
まずはこんな感じの簡単な関数:
let () = let f x = x + 1 in print_int @@ f 5
ocamlopt -c -dlambda localfunc.ml
などとすると:
(seq (let (*match*/84 = (apply (field 43 (global Stdlib!)) (let (x/85 =[int] 5) (+ x/85 1)))) 0a) 0a)
見てのとおり、関数定義が消え、呼び出しがlet
に変換される、というインライン化が行われている。
ちなみにlambda IR変換ではいくつかの最適化も行われる。そんな最適化をする前の「変換しただけのlambda IR」をみたい場合は-dlambda
ではなく-drawlambda
フラグを渡す。
ocamlopt -c -drawlambda localfunc.ml
の結果:
(seq (let (*match*/84 = (let (f/80 = (function x/82[int] : int (+ x/82 1))) (dirapply (field 43 (global Stdlib!)) (apply f/80 5)))) 0a) 0a)
しっかり関数定義・呼び出しが残っているのがわかる。興味深いポイントとしては:
(function x/82[int] : int (+ x/82 1))
のように整数を受け取る・返す場合は型アノテーションが残っている。返り値が文字列などの場合はこのような型アノテーションはないので、=[int]
と同じくboxing省略の関係なのだと思うがまだよくわからない- 見慣れない
dirapply
というキーワードが(print_int
呼び出しのために)出てくる。apply
との違いはよくわからない。direct applyの意味だと思うが・・・
引数2のローカル関数
引数2の場合も前回と同様:
let () = let f x y = x + y in print_int @@ f 1 2
-dlambda
だと:
(seq (let (*match*/85 = (apply (field 43 (global Stdlib!)) (let (x/86 =[int] 1 y/87 =[int] 2) (+ x/86 y/87)))) 0a) 0a)
やはり関数定義・呼び出しが変換で消えている。
-drawlambda
だと残っている&型アノテーションつき:
(seq (let (*match*/85 = (let (f/80 = (function x/82[int] y/83[int] : int (+ x/82 y/83))) (dirapply (field 43 (global Stdlib!)) (apply f/80 1 2)))) 0a) 0a)
ローカル関数の部分適用
先ほどのローカル関数を部分適用したものを別の変数に束縛してみる:
let () = let f x y = x + y in let g = f 1 in print_int @@ g 2
すると-dlambda
でも関数定義・呼び出しのまま残る:
(seq (let (*match*/86 = (let (f/80 = (function x/82[int] y/83[int] : int (+ x/82 y/83)) g/84 = (apply f/80 1)) (apply (field 43 (global Stdlib!)) (apply g/84 2)))) 0a) 0a)
別のIR変換ステップ(例えばclosureつきのclambdaへの変換)で最適化される可能性はあるが、少なくともlambda IRの段階ではこのような簡単な部分適用でも最適化が行われなくなる、ということは面白い。
引数が関数適用の結果
関数の引数部分に別の関数適用の式を入れてみる:
let () = let f x y = x + y in let g x = x - 1 in print_int @@ f 1 (g 2)
この場合は-dlambda
だと普通に関数がインライン化されてlet
に置き換わる:
(seq (let (*match*/88 = (apply (field 43 (global Stdlib!)) (let (x/90 =[int] 1 y/91 =[int] (let (x/89 =[int] 2) (- x/89 1))) (+ x/90 y/91)))) 0a) 0a)
f
の適用とg
の適用で別々のネストしたlet
式になっている。(よく考えると自然な変換ではある)
関数定義内で別のローカル関数を使う
まずローカル関数g
を定義し、それをローカル関数f
の定義内で使う:
let () = let g x = x + 1 in let f x y = x + (g y) in print_int @@ f 1 2
-dlambda
でコンパイルすると関数定義は両方消える:
(seq (let (*match*/88 = (apply (field 43 (global Stdlib!)) (let (x/90 =[int] 1 y/91 =[int] 2) (+ x/90 (+ y/91 1))))) 0a) 0a)
思ったよりシンプルな形になっている。
(let (x/90 =[int] 1 y/91 =[int] 2) (+ x/90 (+ y/91 1)))
の部分が
(let (x/90 =[int] 1 y/91 =[int] 2) (+ x/90 (let (x/92 =[int] y/91) (+ x/92 1))))
になるかと思っていた。
変数を別の変数に束縛
関数関係なくこのようなコードを試してみたら:
let () = let x = 1 in let y = x in print_int @@ x + y
-dlambda
だと:
(seq (let (*match*/83 = (let (x/80 =[int] 1) (apply (field 43 (global Stdlib!)) (+ x/80 x/80)))) 0a) 0a)
とy
が消えている。
-drawlambda
だと:
(seq (let (*match*/83 = (let (x/80 =[int] 1 y/81 =[int] x/80) (dirapply (field 43 (global Stdlib!)) (+ x/80 y/81)))) 0a) 0a)
とy
が残っているので、lambda IR内での最適化として意味のないletによる変数束縛は省略されるようだ。
モジュールトップレベルでの関数定義
let f x = x + 1 let () = print_int @@ f 5
-dlambda
:
(seq (let (f/80 = (function x/82[int] : int (+ x/82 1))) (setfield_ptr(root-init) 0 (global Func!) f/80)) (let (*match*/85 = (apply (field 43 (global Stdlib!)) (apply (field 0 (global Func!)) 5))) 0a) 0a)
正直あまり面白みはない。トップレベルにある関数に関してはlambda IRの段階ではインライン化は効かない。
次回
次はタプルがどうコンパイルされるかをみていく。
OCamlのlambda IRをいじる(変数)
前回に続いて、モジュール内で変数を定義して使うプログラムがどのようなlambda IRにコンパイルされるかを見ていく。
ローカル変数1つ
OCamlで変数を定義する場合、モジュールのトップレベルでの定義と、ある式の一部としてlet x = y in z
のx
のような定義の二種類がある。
まずは後者から見ていく。
OCamlコード:
let () = let x = "hi" in print_endline x
コンパイルされたlambda IR:
(seq (let (*match*/82 = (let (x/80 = "hi") (apply (field 45 (global Stdlib!)) x/80))) 0a) 0a)
前回
(let (.. = .. ...) X)という形のXの部分で「変数を束縛した環境で評価するべき式」が入る
と書いた。(let (x/80 = "hi") (apply (field 45 (global Stdlib!)) x/80))
がまさにそれで(apply (field 45 (global Stdlib!)) x/80)
がXにあたる。
x/80
という変数を"hi"という文字列に束縛した上でStdlib
モジュールのインデックス45であるprint_endline
関数をそのx/80
変数の内容に適用している。
ローカル変数複数
複数のローカル変数を定義する場合:
let () = let x = "hi" in let y = "ho" in let z = x ^ y in print_endline z
(seq (let (*match*/84 = (let (x/80 = "hi" y/81 = "ho" z/82 = (apply (field 27 (global Stdlib!)) x/80 y/81)) (apply (field 45 (global Stdlib!)) z/82))) 0a) 0a)
OCamlだと別のlet
を連続して書いていく形になるが、lambda IRでは単一の(let ...)
に中で多重変数束縛が行われる。また、変数名のユニーク化のための数字の割り当てのロジック(新しい変数に割り振るごとに+1されていく)がはっきりわかりやすい。
ローカル変数が整数
ローカル変数が整数の場合、少しだけlambda表現が異なる。
let () = let x = 1 in print_int x
(seq (let (*match*/82 = (let (x/80 =[int] 1) (apply (field 43 (global Stdlib!)) x/80))) 0a) 0a)
このように(let (... =[int] ...) ...)
のような形にコンパイルされる。OCamlでは整数やcharは(他の多くのデータと違って)box化されないので、lambda以降のコンパイルで特別な扱いをできるよう=[int]
としているのだろうか?
モジュールレベル定義
最後にモジュールのトップレベルでの変数定義:
let x = 1 let y = 2 let () = print_int @@ x + y
(seq (let (x/80 =[int] 1) (setfield_ptr(root-init) 0 (global Local4!) x/80)) (let (y/81 =[int] 2) (setfield_ptr(root-init) 1 (global Local4!) y/81)) (let (*match*/85 = (apply (field 43 (global Stdlib!)) (+ (field 0 (global Local4!)) (field 1 (global Local4!))))) 0a) 0a)
OCamlコードのlet ...
が(seq ... 0a)
の中で(let ... ...)
に1対1で対応している。いったんx/80
のような変数に束縛した上で、(setfield_ptr(root-init) 0 (global Local4!) x/80)
などで現在定義中のモジュールのフィールドとして追加している。(このコードのファイル名はlocal4.ml
にしてあるのでモジュール名もLocal4
になる)
少し気になるのは、これlet
する意味があるのか?というところ。変数束縛した後に、その変数を使ってモジュールのフィールドに値を格納するだけなので、
(let (x/80 =[int] 1) (setfield_ptr(root-init) 0 (global Local4!) x/80))
を
(setfield_ptr(root-init) 0 (global Local4!) 1)
にしてしまっても差し支えない気もするのだが・・・ lambda IRの時点ではまだやっていない最適化なのか、それともlet
を経由することがメモリ上の意味を持つのか?Real World OCamlのRuntime関連の部分を熟読するとわかるのかもしれない。
次回
次は関数定義がどうlambda IRにコンパイルされるかを見ていく。
OCamlのlambda IRをいじる(はじめに)
OCamlのコンパイラの話題でよく聞くflambdaという最適化ステップについて資料を読んでいた:
https://ocaml.org/meetings/ocaml/2013/slides/chambart.pdf
スライド5にコンパイラの中間表現のステップと説明が書いてあったのが面白かった。
parse tree: AST typed tree: AST with types lambda: untyped lambda clambda: lambda + closures cmm: simple Clike mach: instruction graph (llvmlike) lin: almost like assembly
とのこと。大体の流れは知っていたものの、はっきりと書いてあるのを読むと勉強になる。
そう言えば昔@no_maddoさんのブログでこんな記事を読んだ:
この記事に出ているとおり、ocamlc
やocamlopt
でコンパイルする場合-dlambda
や-dclambda
といったコンパイラフラグを渡すことで中間表現が出力される。これを眺めていろいろあーだこーだと考えてみたい。
理由としては:
- OCaml使いとしてはどうプログラムがコンパイルされているかをより理解したい
- js_of_ocamlやbucklescriptは中間表現やバイトコードからJSにコンパイルするので、そこについての理解を深めたい
- 実用的なコンパイラの動きを知りたい
- 自作言語の中間表現として使ってみたい
などが全部当てはまる。
とりあえず以下のことを調べてみたい:
- 簡単な
print
だけのプログラム - 変数(モジュールレベル・ローカル)
- 関数(モジュールレベル・ローカル・部分適用)
- 再帰関数
- データ(タプル、リスト、レコード、代数的データ型)
- ミュータブルデータ(ref・array)
- モジュール・ファンクタ
- 複数ファイルにまたがるプログラム
ちなみにここらへんはもちろんコンパイラのバージョンに依存するところが大きい。私が現在使っているのはOCaml 4.11.1である。
何もしないプログラム1
まずはこれだけのプログラムをempty.ml
として保存し、lambda IRに出力してみる:
let () = ()
見ての通り何もしない。
これをocamlopt -c -dlambda empty.ml
でコンパイルすると:
(seq (let (*match*/81 = 0a) 0a) 0a)
こんな出力になる。
初めてこれだけを見るとわかりにくいが、まず大事なポイントとしては
0a
は()
に対応している- モジュールは
(seq ... 0a)
という形にコンパイルされる(...
の中には1個以上のS式が入る) let () = ()
は(let (*match*/81 = 0a) 0a)
にコンパイルされている*match*/xx
というのが左側の()
に対応している。xx
の部分は「変数名がユニークであることを保証する」α変換処理によって振られる数字。(let (.. = .. ...) X)
という形のX
の部分で「変数を束縛した環境で評価するべき式」が入るのだが、このケースだと何も評価する必要がないので()
を表す0a
が入っている
ちなみに最後のポイントに関して、(.. = ..)
で=が中置演算子になっているのはS式から外れてしまって少し悲しい気がする。まあ人が読みやすいようにADTを文字列出力しているだけだからどうでもいいのだけど・・・
何もしないプログラム2
次にこんなプログラムについて考える:
let () = () let () = ()
これはこんな感じに出力される:
(seq (let (*match*/81 = 0a) 0a) (let (*match*/83 = 0a) 0a) 0a)
seq
の中にほぼ同一の要素が増えただけ。*match*/xx
に振られた数字が違っていてユニークな変数名になっているのがポイント。
print_int
次に整数を標準出力に出すプログラム:
let () = print_int 1
(seq (let (*match*/81 = (apply (field 43 (global Stdlib!)) 1)) 0a) 0a)
今まで(*match*/81 = 0a)
だったところの0a
が(apply (field 43 (global Stdlib!)) 1)
に置き換わっている。
ポイントは:
(global X!)
でX
モジュールにアクセス(field n X)
でXモジュールのn
番目のフィールドにアクセス(モジュール以外のデータ構造でも可能)(apply X Y)
で関数Xに引数Yを適用
という流れ。print_int
はStdlib
モジュールのインデックス43に入っている、というのがlambda IRに変換される時点で解決済みなわけだ。なのでprint_int
という変数名・文字列はIRには全く出てこない。
次回
次回はモジュール内で定義された変数がどうlambda IRで扱われるかを見ていく。
PythonでリテラルなしIfなし変数束縛なしFizzBuzzワンライナー〜ラムダの力を讃えよ〜
前回こう書いた通り:
追記:前回の記事に書き忘れたが、walrus operatorことAssignment Expressionsを多用しているのでPython3.8以降が必要。
これまで見てきたワンライナーはPython3.8から導入された変数束縛式:=
を使っていて、そのせいで最近のPythonバージョンに依存している。
このコードだと:
(f:=lambda *a,x=id,Fizz=id,Buzz=id:(z:=len(a)) or print((k:=sorted(f.__kwdefaults__))[f(f)]*(x%f(f,f,f)==z)+k[z]*(x%f(f,f,f,f,f)==z) or x) or f(x=x+f(f)))(x=f(f))
— zehnpaard (@zehnpaard) April 25, 2021
(f:=lambda ...)
、(z:=len(a))
そして(k:=sorted(f.__kwdefaults__))
の三ヶ所で利用している。
そのうち後者二つは文字数を減らすために使っているようなもので、冗長でいいなら省略できる:
(f:=lambda *a,x=id,Fizz=id,Buzz=id: len(a) or print( sorted(f.__kwdefaults__)[f(f)]*(x%f(f,f,f)==len(a))+ sorted(f.__kwdefaults__)[len(a)]*(x%f(f,f,f,f,f)==len(a)) or x) or f(x=x+f(f)))(x=f(f))
(読みやすいように改行付き)
しかしf
という関数名をつけるのは再帰的な定義の要。やっぱり変数束縛は必要だろう。
そのように安直に考えていると
ラムダを崇めよ、ラムダの力を讃えよ
どこからともなく迷える子羊を導く声が聞こえた。多分内なるアロンソ・チャーチかハスケル・カリーが語りかけてきているのだろう。あるいはラムダマンことフィル・ワドラー("Phil Wadler Lambda Man"で検索してください)かもしれない。
そう、ラムダに魂を惹かれた者なら誰でも知っている。再帰がしたいならFixed Pointを使えばいいじゃない。
つまり別の無名関数を使って、再帰させたい無名関数を自己適用することで、関数内でその関数自身が引数として束縛されている状況を作り出せる。
無名関数の中で使われるf
を、外部環境を参照するのではなく、この無名関数の仮引数として追加してやる:
lambda *a,f=id,x=id,Fizz=id,Buzz=id: len(a) or print( sorted(f.__kwdefaults__)[f(f)]*(x%f(f,f,f)==len(a))+ sorted(f.__kwdefaults__)[len(a)]*(x%f(f,f,f,f,f)==len(a)) or x) or f(f=f,x=x+f(f))
(最後の再帰的呼び出しの時にもちゃんとf=f
と渡してやるのを忘れずに)
そしてこの無名関数を引数に受け取り、必要な引数を与えて呼び出すための別の無名関数を使えばいい:
(lambda f:f(f=f,x=f(f)))( lambda *a,f=id,x=id,Fizz=id,Buzz=id: len(a) or print( sorted(f.__kwdefaults__)[f(f)]*(x%f(f,f,f)==len(a))+ sorted(f.__kwdefaults__)[len(a)]*(x%f(f,f,f,f,f)==len(a)) or x) or f(f=f,x=x+f(f)))
ちょっと文字数を減らしたいならついでにk
も仮引数に追加できる:
(lambda f:f(f=f,k=sorted(f.__kwdefaults__),x=f(f)))( lambda *a,f=id,k=id,x=id,Fizz=id,Buzz=id: len(a) or print( k[f(f)]*(x%f(f,f,f)==len(a))+ k[len(a)]*(x%f(f,f,f,f,f)==len(a)) or x) or f(f=f,k=k,x=x+f(f)))
というわけでWalrus Operator使わないで202文字にまとまる:
(lambda f:f(f=f,k=sorted(f.__kwdefaults__),x=f(f)))(lambda *a,f=id,k=id,x=id,Fizz=id,Buzz=id:len(a) or print(k[f(f)]*(x%f(f,f,f)==len(a))+k[len(a)]*(x%f(f,f,f,f,f)==len(a)) or x) or f(f=f,k=k,x=x+f(f)))
— zehnpaard (@zehnpaard) April 26, 2021
リテラルも変数束縛もいらない。ラムダさえあればいい・・・
PythonでリテラルなしIfなしFizzBuzzワンライナー
前回の記事のFizzBuzzコードをちょこっといじってこんな感じで満足していた:
リテラルを使わず簡潔な高可読性FizzBuzzが書けるプログラマです🤥
— zehnpaard (@zehnpaard) April 23, 2021
(f:=lambda *a,A=id,Fizz=id,Buzz=id:len(a) or (k:=sorted(f.__kwdefaults__)) and print(A if (x:=A%f(f,f,f))*(y:=A%f(f,f,f,f,f)) else k[f(f)] if x else k[f(f,f)] if y else k[f(f,f)]+k[f(f)]) or f(A=A+f(f)))(A=f(f)) https://t.co/W5Wph2zgwh
そうしたらこんなツイートが:
ifやswitch/matchを使わないfizzbuzz
— がくぞ (@gakuzzzz) April 25, 2021
え、IfもなしでFizzBuzzを・・・?
出来らあ!
と啖呵を切って考えてみる。まあ前回のコードでも利用していたand/orの特性を使えばifは当然省けるはず。
Pythonだと真偽値以外もTruthinessを持ち、特に空文字列は偽なので("Fizz" if x%3==0 else "")+("Buzz" if x%5==0 else "") or x
のようなロジックを使うのが一番スマート。
もう一つハック的なポイントとして、PythonでTrue
とFalse
はほぼ1
と0
と同等なので("Fizz" * (x%3==0))+("Buzz" * (x%5==0)) or x
と書ける。
これでコア部分のロジックはif
なしでいけそう。
前回の記事でも紹介した通り、"Fizz", "Buzz", 3, 5はリテラルなしで簡単に作れる。しかし0は(無名関数の定義の仕方のせいで)作れなくなっている。f(f)-f(f)
のようにして作ってもいいのだが、長くなるのでイマイチ。
まず考えたのは関数内の式の順番を入れ替えるやり方:
(f:=lambda *a,A=id,Fizz=id,Buzz=id:(A!=id and ((k:=sorted(f.__kwdefaults__)) and print(k[f(f,f)]*(A%f(f,f,f)==f())+k[f(f)]*(A%f(f,f,f,f,f)==f()) or A) or f(A=A+f(f)))) or len(a))(A=f(f)) https://t.co/wAZB2sVvdY
— zehnpaard (@zehnpaard) April 25, 2021
FizzBuzzのプリント・ループ処理に入る条件をA!=id
にして、len(a)
は関数の最後に持ってきた。これでf()
が0になる。
しかし、よく考えるとf()
で0を表現するまでもなく、プリント・ループ処理に入った時点でlen(a)
という0の値をすでに評価していることに気づいた。この値を変数束縛して使ってしまえばいい。
さらに二つ修正点がある:
X if Y else Z
という構文を使っていた時はX
やZ
の部分は評価されないケースがあった。それに対してX + Y or Z
の場合、X
の部分は必ず評価される。なのでX
に(k:=sorted(f.__kwdefaults__))
という変数束縛を持ってこれる- 0が表現できるので仮引数をソートして"Buzz"が先頭に来ても問題ない。ので仮引数に
A
を使う必要がなくなり、"Fizz"と"Buzz"をアクセスするインデックスに1と0を使える(文字数を減らせる)。
これらを踏まえて、とりあえず現在の最終形:
(f:=lambda *a,x=id,Fizz=id,Buzz=id:(z:=len(a)) or print((k:=sorted(f.__kwdefaults__))[f(f)]*(x%f(f,f,f)==z)+k[z]*(x%f(f,f,f,f,f)==z) or x) or f(x=x+f(f)))(x=f(f))
— zehnpaard (@zehnpaard) April 25, 2021
if
なしという制約がつく前に比べて文字数が減ってる・・・ (この記事の冒頭のコードが211文字、最終形のコードが162文字)
制約がついて工夫しないといけなくなるといいアイデアが出る、というケース。
追記:前回の記事に書き忘れたが、walrus operatorことAssignment Expressionsを多用しているのでPython3.8以降が必要。
PythonでリテラルなしFizzBuzzワンライナー
ツイッターでこういう話が出ていた(@nishio さん、面白いお題をありがとうございます!):
リテラル禁止FizzBuzz大会、開幕!(待て https://t.co/zpIirtNpmO
— nishio hirokazu (@nishio) April 16, 2021
class Fizz:
— zehnpaard (@zehnpaard) April 16, 2021
def Buzz(*a): return len(a)
b=https://t.co/H1SEH2bvbd
x,y,z,fs,bs=b(b),b(b,b,b),b(b,b,b,b,b),Fizz.__name__,b.__name__
while b:
print(x if x%y and x%z else bs if x%y else fs if x%z else fs+bs)
x+=b(b) https://t.co/KnMqQjZf77
数字は「*args
で可変長引数をとって、その長さを返す」関数で表現できる。
b = Fizz.Buzz one = b(b) two = b(b,b) three = b(b,b,b)
といった使い方ができる(ちなみに引数の値自体はなんでもいい)。
文字列はオブジェクトの__name__
を取れば大丈夫。
ガハハ、勝ったな、と思ったら:
というわけで「Pythonで、リテラル禁止、改行禁止、1ツイートに収まるFizzBuzz」という課題は手慣れたPythonistaなら30分で作れることが実証されました https://t.co/qOrxvhMyhJ
— nishio hirokazu (@nishio) April 16, 2021
この縛りだとクラスや関数をclass/def
で定義できなくなってしまう。
ならば・・・
Pythonワンライナー
— zehnpaard (@zehnpaard) April 17, 2021
(g:=lambda x: print(x if (fs:=list(f.__kwdefaults__)[f()]) and (bs:=list(f.__kwdefaults__)[f(f)]) and x%(y:=f(f,f,f)) * x%(z:=f(f,f,f,f,f)) else fs if x%z else bs if x%y else fs + bs) or g(x+f(f))) and (f:=lambda *a,Fizz=g,Buzz=g:len(a)) and g(f(f)) https://t.co/KnMqQjHDIx
私が勝手に「Guidoの置き土産」と呼んでいるWalrus Operator:=
を使えば一つの式の中で変数束縛し放題。「改行なし」をさらに制限して「式一つ」で出来てしまう。
X and Y and Z
はX
とY
がTruthy
な値なら順次に評価していって最終的な値はZ
になる。
lambda
を使った無名関数二つにWalrus Operatorでg
とf
という名前を束縛している。
順番が前後するが、まずf
関数を見てみる。これは必要な数字と文字列を得るための関数で、「*args
で可変長引数をとって、その長さを返す」アイディアはそのまま、そしてlambda *a, Fizz=id, Buzz=id:...
とFizz
、Buzz
をデフォルト値のあるkwargsとして追加している。これでf.__kwdefaults__
で{ "Fizz": ..., "Buzz": ... }
といったdictionaryが入手できて、キーから文字列が得られる。
g
関数はFizzBuzzロジックと再帰によるループを、X if Y else Z
やand/or
、walrus operatorを使いまくって一つの式に詰め込んでいる。
個人的に一番のポイントだったのはx%(y:=f(f,f,f)) * x%(z:=f(f,f,f,f,f))
とx%3が0だった時に右側がショートサーキットで飛ばされないよう、and
ではなく*
にしてあるところ。
なんでf
がg
のあとに出てくるかというと、もともとf:=lambda *a,Fizz=id,Buzz=id:...
と定義していたのだが(組み込み関数の中でid
が一番短い)、g
と定義順を逆にすれば文字数が削れるのと関係ないid
が出てこなくて済む、ということに気づいて順番を入れ替えたから。Pythonでは関数のデフォルト引数は「件数呼び出し時」ではなく「関数定義時」に評価されるので、すでに定義済みの変数しか使えない(例えば再帰的にデフォルト引数をf
にすることもできない)。
これでg(f(f))
すると1からmax recursion depth
に到達する995までの数字のFizzBuzzが表示される。
その後少し修正を加えて、今現在の最終形はこちら:
束縛する名前を減らしてみた
— zehnpaard (@zehnpaard) April 17, 2021
(f:=lambda *a,A=id,Fizz=id,Buzz=id: len(a) if a else print(A if (kw:=sorted(f.__kwdefaults__)) and A%f(f,f,f) and A%f(f,f,f,f,f) else kw[f(f,f)] if A%f(f,f,f,f,f) else kw[f(f)] if A%f(f,f,f) else kw[f(f,f)]+kw[f(f)]) or f(A=A+f(f))) and f(A=f(f))
変更点は以下の通り:
- 単一の無名関数に詰め込んだ(そのため新しいkwargsである
A
を定義している。f(f,f,f)
のように引数を普通に入れる場合は整数を返し、f(A=1)
のようにすれば先ほどのg
関数のようにFizzBuzzループが始まる)。ちなみに可変長引数a
の長さを条件にどっちの処理を行うかの分岐をするという実装の関係でf()
で0が表現できなくなっている。そのせいで新しいkwargsはFizzとBuzzよりもlexographicに小さい文字列である必要が出てくるので大文字のA
にしてある。また無名関数が一つになった関係でプレースホルダーとしてid
を使う必要が出てきたのは無念。 list(f.__kwdefaults__)
だとどの順番で文字列が得られるかが微妙に実装依存な気もする(最近のPythonでは違うとは思うが)のでsorted(f.__kwdefaults__)
にして文字列の順番が一意に定まるようにした。- walrus operatorによる変数束縛を減らした。束縛されるのは
f
関数自体とkw:=sorted(f.__kwdefaults___)
の二つだけ。前者は(特に再帰のために)絶対必要(いや、やろうと思えばFix operator的なことができるのか?)で、後者はツイートに入れるための文字数制限の関係で必要。
リテラル禁止FizzBuzz、問題としてなかなか楽しかったのでもっと流行ってもよかったのになー、と少し寂しい気持ちがある。