2022/08/22

特に書くことがないので先日書いた無名関数だけでFizzBuzzを解く方法を紹介します


↓のコードをブラウザーのコンソールに貼ってみてください。

const Z = (
  A => (
    B => A(c => B(B)(c))
  )(
    B => A(c => B(B)(c))
  )
)
const CAR = i => i[0]
const CDR = i => i[1]
const RANGE = n => Z(
  X => i => i == 1 ? [i, []] : [i, X(i - 1)]
)(n)

const MAP = (
  F => l => Z(
    X => m => CDR(m).length == 0 ? [F(CAR(m)), []] : [F(CAR(m)), X(CDR(m))]
  )(
    l
  )
)

const FIZZBUZZ = x => (
  MAP(
    i => i % 15 ? i % 3 ? i % 5 ? i : 'Buzz' : 'Fizz' : 'FizzBuzz'
  )(
    RANGE(x)
  )
)

console.log(FIZZBUZZ(100).flat(Infinity).reverse())

おそらくFizzBuzzがきちんと解けていると思います。
コードを軽く見るとわかるのですが、プログラムでは関数以外の一切の制御構造と変数を利用していません。
ZやCAR,CDRの名前に変数を使っているじゃないかと思われるかもしれませんが、これはあくまで名前付けのために書いてあるだけで、変数を利用せずにプログラムを書き下すことは可能です。
下記がその例です。

(x=>((Z=>(F=>l=>Z(X=>m=>m[1].length?[F(m[0]),X(m[1])]:[F(m[0]),[]])(l))(i=>i%15?i%3?i%5?i:'Buzz':'Fizz':'FizzBuzz')((n=>Z(X=>i=>i==1?[i,[]]:[i,X(i-1)])(n))(x)))(A=>(B=>A(c=>B(B)(c)))(B=>A(c=>B(B)(c))))))(100).flat(1/0).reverse()

制御構文(map等も含めて)を使わずにFizzBuzzを解くために、まず普通にFizzBuzzを解いてみることにします。

[...Array(101).keys()].slice(1).map(i=>i%15?i%3?i%5?i:'Buzz':'Fizz':'FizzBuzz')

簡単ですね。
[...Array(101).keys()].slice(1)pythonでいうところのrange(1, 101)と同じです。
また、.map(i=>i%15?i%3?i%5?i:'Buzz':'Fizz':'FizzBuzz')もそのままそれぞれに対してFizzBuzz判定を行います。


処理をわかりやすくするためにわかりにくいコードを書き換えてしまいましょう。

const range = (s, e) => [...Array(e).keys()].slice(s)
const fb = i => i%15?i%3?i%5?i:'Buzz':'Fizz':'FizzBuzz'
range(1, 101).map(fb)

ここからはrange/mapを無名関数に落とし込むわけですが、ループがないためまずループを再帰関数で作成するところから始めます。
通常の関数で再帰関数を書くと次のようになります。この関数は階乗を求める関数ですが、関数の内部で自分自身の名前を参照しています。無名関数に名前をつけることはできないため、これではいけません。

function f(n) {
  if (n < 2) return 1;
  return n * f(n - 1);
}

ここで活用できるのがZコンビネーターです。詳しい説明は他の記事にまかせるとして誤解を恐れずにいうと、関数を渡すと名前をつけた自分自身を返してくれる関数です。

実際のZコンビネーターが↓の通りです。

const Z = (
  A => (
    B => A(c => B(B)(c))
  )(
    B => A(c => B(B)(c))
  )
)
const f = Z(
  X => n => {
    if (n < 2) return 1;
    return n * X(n -1)
  }
)

Zコンビネーターで囲むことによって関数名に依存する処理はなくなりました。
これで自由に再帰処理を書けるようになったので、ループも自由に記述できます。

range(に相当する)関数を作成します。
Listを返す関数なので、ループを使って作成するのですが、せっかくなのでlambda計算でリストを表す方法を参考にしてみたいと思います。

スコットエンコーディングというらしいですが、よくわからんので、特に解説はしません。
無名関数で実現できることは偉い人が証明しているはずなので、javascriptのArrayで記述しました。
また、リストの順番が逆な気もするのですが、気にしないことにしました。

例:
[1,2,3]→[3,[2,[1]]]

例えば、引数として受け取ったリストに新しく要素を追加する場合
l => [1,l]
とすれば良いんじゃないかなと思います

そしてできたrange関数が以下の通りです。

const RANGE = n => Z(
  X => i => i == 1 ? [i, []] : [i, X(i - 1)]
)(n)

Zコンビネーターで再帰処理を記述していること以外はとても素直なコードになっていると思います。
また、リストの最後は空のArrayです。

例えばrange(4)と呼び出すと、[4,[3,[2,[1,[]]]]]というリストを得ることができます。

次にmapを作成します。とは言っても、↑のようにリストを受け取ってリストの最後まで、順に関数を適用した新しいリストを作成するだけです。

const MAP = (
  F => l => Z(
    X => m => CDR(m).length == 0 ? [F(CAR(m)), []] : [F(CAR(m)), X(CDR(m))]
  )(
    l
  )
)

ここで、リストの最初の要素と最初の要素を取り除いたリストを返す関数CAR/CDRを定義しました。lispみたいなやつだと思います。(lispは書いたことがないので…)

例えばCAR([4,[3,[2,[1,]]]])は4を返して、CDR([4,[3,[2,[1,]]]])は[3,[2,[1,[]]]]]を返します。

Javascriptラムダ式は複数の引数を取ることができるのですが、今回はλらしく一つの引数だけ受け取るようにしたかったので、カリー化的なことをしている…と思います。

これで、range/mapが完成しましたので、次のようなコードが動作する様になります。

// 冒頭のコードを貼り付ける
MAP(i=>i*2)(RANGE(6))

値をfizzbuzz形式に変換する関数は const fb = i => i%15?i%3?i%5?i:'Buzz':'Fizz':'FizzBuzz'で動作するので、引数の数までのFizzBuzzのリストを返すFIZZBUZZ関数は次のように定義できます。

const FIZZBUZZ = x => (
  MAP(
    i => i % 15 ? i % 3 ? i % 5 ? i : 'Buzz' : 'Fizz' : 'FizzBuzz'
  )(
    RANGE(x)
  )
)

帰ってくるリストにFIZZBUZZ(100).flat(1/0).reverse()を適用して、読みやすくできます。

まとめ
なんかよくわからんけどλすげー