Arantium Maestum

プログラミング、囲碁、読書の話題

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 zxのような定義の二種類がある。

まずは後者から見ていく。

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 C­like
mach: instruction graph (llvm­like)
lin: almost like assembly

とのこと。大体の流れは知っていたものの、はっきりと書いてあるのを読むと勉強になる。

そう言えば昔@no_maddoさんのブログでこんな記事を読んだ:

no-maddojp.hatenablog.com

この記事に出ているとおり、ocamlcocamloptコンパイルする場合-dlambda-dclambdaといったコンパイラフラグを渡すことで中間表現が出力される。これを眺めていろいろあーだこーだと考えてみたい。

理由としては:

などが全部当てはまる。

とりあえず以下のことを調べてみたい:

ちなみにここらへんはもちろんコンパイラのバージョンに依存するところが大きい。私が現在使っているのは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_intStdlibモジュールのインデックス43に入っている、というのがlambda IRに変換される時点で解決済みなわけだ。なのでprint_intという変数名・文字列はIRには全く出てこない。

次回

次回はモジュール内で定義された変数がどうlambda IRで扱われるかを見ていく。

PythonでリテラルなしIfなし変数束縛なしFizzBuzzワンライナー〜ラムダの力を讃えよ〜

前回こう書いた通り:

追記:前回の記事に書き忘れたが、walrus operatorことAssignment Expressionsを多用しているのでPython3.8以降が必要。

これまで見てきたワンライナーはPython3.8から導入された変数束縛式:=を使っていて、そのせいで最近のPythonバージョンに依存している。

このコードだと:

(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文字にまとまる:

リテラルも変数束縛もいらない。ラムダさえあればいい・・・

PythonでリテラルなしIfなしFizzBuzzワンライナー

前回の記事FizzBuzzコードをちょこっといじってこんな感じで満足していた:

そうしたらこんなツイートが:

え、IfもなしでFizzBuzzを・・・?

出来らあ!

と啖呵を切って考えてみる。まあ前回のコードでも利用していたand/orの特性を使えばifは当然省けるはず。

Pythonだと真偽値以外もTruthinessを持ち、特に空文字列は偽なので("Fizz" if x%3==0 else "")+("Buzz" if x%5==0 else "") or xのようなロジックを使うのが一番スマート。

もう一つハック的なポイントとして、PythonTrueFalseはほぼ10と同等なので("Fizz" * (x%3==0))+("Buzz" * (x%5==0)) or xと書ける。

これでコア部分のロジックはifなしでいけそう。

前回の記事でも紹介した通り、"Fizz", "Buzz", 3, 5はリテラルなしで簡単に作れる。しかし0は(無名関数の定義の仕方のせいで)作れなくなっている。f(f)-f(f)のようにして作ってもいいのだが、長くなるのでイマイチ。

まず考えたのは関数内の式の順番を入れ替えるやり方:

FizzBuzzのプリント・ループ処理に入る条件をA!=idにして、len(a)は関数の最後に持ってきた。これでf()が0になる。

しかし、よく考えるとf()で0を表現するまでもなく、プリント・ループ処理に入った時点でlen(a)という0の値をすでに評価していることに気づいた。この値を変数束縛して使ってしまえばいい。

さらに二つ修正点がある:

  • X if Y else Zという構文を使っていた時はXZの部分は評価されないケースがあった。それに対してX + Y or Zの場合、Xの部分は必ず評価される。なのでX(k:=sorted(f.__kwdefaults__))という変数束縛を持ってこれる
  • 0が表現できるので仮引数をソートして"Buzz"が先頭に来ても問題ない。ので仮引数にAを使う必要がなくなり、"Fizz"と"Buzz"をアクセスするインデックスに1と0を使える(文字数を減らせる)。

これらを踏まえて、とりあえず現在の最終形:

ifなしという制約がつく前に比べて文字数が減ってる・・・ (この記事の冒頭のコードが211文字、最終形のコードが162文字)

制約がついて工夫しないといけなくなるといいアイデアが出る、というケース。

追記:前回の記事に書き忘れたが、walrus operatorことAssignment Expressionsを多用しているのでPython3.8以降が必要。

PythonでリテラルなしFizzBuzzワンライナー

ツイッターでこういう話が出ていた(@nishio さん、面白いお題をありがとうございます!):

リテラルなしでFizzBuzz?出来らぁ!

数字は「*argsで可変長引数をとって、その長さを返す」関数で表現できる。

b = Fizz.Buzz
one = b(b)
two = b(b,b)
three = b(b,b,b)

といった使い方ができる(ちなみに引数の値自体はなんでもいい)。

文字列はオブジェクトの__name__を取れば大丈夫。

ガハハ、勝ったな、と思ったら:

え、リテラルも改行もなしでFizzBuzzを・・・?

この縛りだとクラスや関数をclass/defで定義できなくなってしまう。

ならば・・・

私が勝手に「Guidoの置き土産」と呼んでいるWalrus Operator:=を使えば一つの式の中で変数束縛し放題。「改行なし」をさらに制限して「式一つ」で出来てしまう。

X and Y and ZXYTruthyな値なら順次に評価していって最終的な値はZになる。

lambdaを使った無名関数二つにWalrus Operatorでgfという名前を束縛している。

順番が前後するが、まずf関数を見てみる。これは必要な数字と文字列を得るための関数で、「*argsで可変長引数をとって、その長さを返す」アイディアはそのまま、そしてlambda *a, Fizz=id, Buzz=id:...FizzBuzzをデフォルト値のあるkwargsとして追加している。これでf.__kwdefaults__{ "Fizz": ..., "Buzz": ... }といったdictionaryが入手できて、キーから文字列が得られる。

g関数はFizzBuzzロジックと再帰によるループを、X if Y else Zand/or、walrus operatorを使いまくって一つの式に詰め込んでいる。

個人的に一番のポイントだったのはx%(y:=f(f,f,f)) * x%(z:=f(f,f,f,f,f))とx%3が0だった時に右側がショートサーキットで飛ばされないよう、andではなく*にしてあるところ。

なんでfgのあとに出てくるかというと、もともとf:=lambda *a,Fizz=id,Buzz=id:...と定義していたのだが(組み込み関数の中でidが一番短い)、gと定義順を逆にすれば文字数が削れるのと関係ないidが出てこなくて済む、ということに気づいて順番を入れ替えたから。Pythonでは関数のデフォルト引数は「件数呼び出し時」ではなく「関数定義時」に評価されるので、すでに定義済みの変数しか使えない(例えば再帰的にデフォルト引数をfにすることもできない)。

これでg(f(f))すると1からmax recursion depthに到達する995までの数字のFizzBuzzが表示される。

その後少し修正を加えて、今現在の最終形はこちら:

変更点は以下の通り:

  • 単一の無名関数に詰め込んだ(そのため新しい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、問題としてなかなか楽しかったのでもっと流行ってもよかったのになー、と少し寂しい気持ちがある。