🏌️

Functional() (esolang)の紹介とFizzBuzz 273bytes 実装(後日269に更新)

2022/12/03に公開

はじめに

これは、Qiitaの「多言語FizzBuzz Advent Calendar 2022」の一環(12/4分)として、esolangの1つであるFunctional()でFizzBuzzを「golf的に(なるべく短く)」実装したものです。
結果、コード文字数273bytesを実現しました。
※2022/12/31追記: その後269bytesに更新したため、その差分についても章を追加しました。

なお、本記事に先駆けてこのコードはStackExchangeにも投稿しています。
また、FizzBuzzの実装を説明するにあたり、軽くFunctional()自体の紹介も兼ねています。

Functional()とは

以前、第7回Esolang Codegolf Contest Writeupで、問題を解く時に初めて触れたのですが、とてもシンプルな関数型言語で、(関数型ではない)Brainf**kと同じように、入出力を含むごく僅かな基本機能 ( 9関数 ) のみで全てを組み立てるという特徴を持っています。
こちらのEsolang Wikiに一通り説明があるのですが、以下でも簡単に紹介したいと思います。

Functional()の文法

まず、Functional()には、トークンの区切りとして働く左右のカッコ (, )、カンマ ,、各種空白文字 ( スペース、タブ、改行等 ) 以外の文字に決まった働きはありません。もちろん、予約語や演算子やリテラル(定数)もありません。

例えば、一般の言語で !eof and s="abc def,ghi"*5 のようなコードがあれば、!, eof, s, =, "abc def", *, 5 と7つのトークンに分割して、その中で and が予約語で、!, =, * が演算子で、"abc def,ghi" が文字列リテラル、5 が数値リテラルと読みたくなるものと思います。
しかしFunctional()では、!eof, and, s="abc, def, ghi"*5 という、空白やカンマ区切りによる5つのトークンに分割され、それぞれが「識別子」( 変数名のようなもの ) として解釈されます。一般に演算子になるような記号も特別扱いはされませんし、クォートも文字列をまとめるような機能を持ちません。
※実はカッコ・カンマについては状況限定で識別子としても使われるのですが、想定外の挙動と推測できるため、ここでは深くツッコミません。

そうすると、予約語もなしで先ほど「入出力を含むごく僅かな基本機能 ( 9関数 )」と紹介したものをどう使うのか、リテラルもなしでデータをどう扱うのか、という疑問が出るかと思います。
前者についてはこちらはコードの先頭でユーザが好みの(重複しない)識別子をカンマ区切りで並べることで、割り当てるようになっています。
つまりこの基本的な機能を呼び出すための識別子はコード毎に変わることになるのですが、それだと説明し辛いので、この記事では 0, 1, =, :, $, &, <, >, ? を割り当てる前提とします。
後者については、データについてもこれら基本機能を割り当てた識別子を駆使して自分で作るしかありません。各識別子は、それらデータを割り当てる変数として働くことになります。
そしてこのデータというのは、Functional()の場合、例外なく「関数」となります。関数として呼び出さないケースもありますので、その時の感覚でデータと言ったり変数と言ったり関数と言ったり呼び分けることもありますが、全て関数としての機能を持つことにご注意ください。

Functional()の基本関数

ではここでは基本関数について説明します。前章の通り、0, 1, =, :, $, &, <, >, ? という識別子を割り当てるものとします。

  • zero(0)/one(1)
    • いずれも引数を2つとる関数です。
    • zeroは2番目の引数を、oneは1番目の引数を返します。つまり、0(a,b) は b を、1(a,b) は a を返します。
    • この2関数は、falsy/truthy ( 判定関数の偽/真 )、I/O処理におけるビットとしての 0,1 を意味するデータとしても使用されます。
    • zeroは、関数呼び出し時に省略した引数、あるいは未定義の変数を参照した時の値としても使われます。
    • 論理値として見た場合、a(b)a(b,0) と同等であり、a=0 ⇒ 0, a=1 ⇒ b が返されますから、a,bの論理and として使えます。また、a(1,b) は a,bの論理or、a(0,1) は aの論理not として使えます。
  • equal(=)
    • 引数を2つとる関数です。
    • 引数同士が等しいか等しくないかに応じて、前述の 1 または 0 を返します。
    • 引数に 0,1 を取る論理演算として見た場合、=(a)=(a,0) と同等であり、これは 0⇔非0(1) の反転、つまり論理not として使えます。また、=(a,b) は論理not xor、=(=(a,b)) あるいは a(=,1)(b) は論理xor として使えます。
  • assign variable(:)/define variable($) ( 以降、variable は省略 )
    • 引数を2つとる関数です。1番目の引数は単なる識別子である必要があります。
      ※識別子以外 ( a() のような関数呼び出し等 ) を1番目引数に指定した場合、本来の機能を発揮せずに、ただ 0 を返します。
    • いずれも1番目の引数として指定した変数(識別子)に、2番目の値を結びつける機能を持ちます。つまり、$(v,1):(v,1) なら、今後変数 v は値(関数) 1 として使うことができますし、$(v,f()):(v,f()) なら、関数 f を呼び出した結果 ( 返り値 ) が変数 v に紐づけられます。
    • いずれも関数としては、この新たに設定された変数自身の値を返します。
      そのため、例えば関数 f を定義し、すぐ呼び出しに使うような場面、つまり $(f,…)f(a,b,c) を立て続けに行う場合、$(f,…)(a,b,c) とまとめることが可能です。
    • 両者の違いは、$ の方が現在のスコープに新しく変数を作り出し、外側の変数をマスクする ( 変数名でのアクセスを不可能とする ) ことにあります。詳しくは「Funcional()での変数スコープ」の章で後述します。
  • function(&)
    • &(引数リスト)( コード ) の形で、ユーザ定義関数を作り出す機能を持つ関数です。
    • 正確には、&(引数リスト) で「関数テンプレート」という種類の関数の作成、関数テンプレート( コード ) でユーザ定義関数の作成という2つの段階を踏みます。
    • 引数リストとして識別子を指定する、つまり &(0個以上の識別子) の形で関数テンプレートが作成されます。この引数として指定した識別子は、関数テンプレートから作成されるユーザ定義関数での仮引数としての変数名を表します。例えば、&(x,y) なら「『x,yという2つの仮引数を持つユーザ定義関数』を生み出すためのテンプレート」、&() なら「『引数なしのユーザ定義関数』を生み出すためのテンプレート」です。
      &(a,b(),c) のように、識別子以外(関数呼び出し等)を混ぜてしまうと、関数テンプレートを生成する代わりに 0 を返します。
    • 関数テンプレートも一種の関数であるため、これを変数に結び付けて再利用することで、同じ引数形式の関数の生成が短縮できます。
      つまり、$(f,&(x,y)( f本体コード ))$(g,&(x,y)( g本体コード )) のように 2つの関数 f,g を作る際、$(T,&(x,y)), $(f,T( f本体コード )), $(g,T( g本体コード )) と、T としてテンプレートを再利用できるということです。
  • read bit(<)/write bit(>) ( 以降、bitは省略 )
    • 前者は引数無し、後者は引数を1つ取ります。
    • 前者は標準入力からのデータの読み込み、後者は標準出力へのデータの書き込みの機能を持ちます。
    • データはいずれもビット単位で扱われます。つまり、1文字(バイト)処理するのに、8回の操作が必要です。
      ※プログラム終了時に > による半端な出力ビットが溜まっている場合は、残りビット 0 を補って最後の1文字が出力されます。
    • < は読み込んだビットの 0,1 に応じて、基本関数の 0,1 を返します。EOFの場合は 0 を返します。> は、引数に指定した 0,非0 に応じて、ビットの 0,1 を書き込みます。
      ※> は引数の内容をそのまま返します。( 前述のWiki では「0 を返す」とあるのですが、おそらく記述ミスです )
    • ビットは下位ビット→上位ビットの順で処理されます。例えば、英字 A ( アスキーコード 0x41 ) を読み込むと 1,0,0,0,0,0,1,0 の順にビットが読み込まれますし、同じ順にビットを書き込むことで A を出力することになります。
  • eof(?)
    • この関数は引数無しです。
    • 標準入力が既にEOFならば 1 を、そうでなければ 0 を返します。

Functional()での処理の流れ

Functional()は全てが関数ですので、いかに関数呼び出しが処理されるか、がポイントになります。その際の処理単位として、リストとコールチェインの2種類があります。

  • リスト
    次項のコールチェインを , ( カンマ ) で区切ってつなげたものを指します。場面に応じて次の2種類の使い方があります。

    • コードとしてのリスト
      ユーザ定義関数の本体部分で使うリストです。例えば &(a,b)( f(a),g(b),h ) のような関数であれば、f(a),g(b),h の3つのコールチェインをつなげた部分が相当します。
      コード全体も、先頭部分が基本関数割り当て用の識別子群になっていることを除けば、これも1つのリストとして記述されます。
    • 関数引数としてのリスト
      関数呼び出し時に指定する引数を記述するためのリストです。例えば f(a(),b(),c) のように関数fを呼び出す時の a(),b(),c の3つのコールチェインをつなげた部分が相当します。

    いずれも、指定されたコールチェインを順々に評価するものですが、前者の場合は最後に評価したコールチェインの値が関数自体の返り値に使われるのに対し、後者の場合は各コールチェインの値が先頭から関数への引数に使われるという違いがあります。

  • コールチェイン
    関数の呼び出しを連続で行うものです。例えばhを返す関数gと、そのgを返す関数fとがあった場合、f(a,b)()(c,d) は、「a,bのリストを関数引数としてfを呼び、次に空の引数で返り値であるgを呼び、最後にc,dのリストを関数引数として返り値であるhを呼ぶ」ように、連鎖的に関数を呼び出すものとなります。なお、xyz のように引数指定のない単一の識別子を指定した場合、関数呼び出しは行われず単にその識別子(変数)に紐づく値が返されます。
    ちなみに、引数の指定は対応する1組のカッコ()で囲って行いますが、例外的に単一の識別子を引数にする場合、カッコを省略することができます。例えば、f(a)(b,c)(d)(e)() のようなコールチェインに対して f a(b,c)d e() といった形での指定が可能です。
    カッコの前後は既に区切られているのが明らかなので、空白文字を追加して更に間を空ける必要はありませんが、fadeの間のようにカッコがない部分では、明示的に空白文字で区切る必要があります。
    注意が必要なのは、f a(b,c) のような形は f(a)(b,c) の省略形であって、f(a(b,c)) にはならないということです。()による関数呼び出しの結合優先度が高い他の言語に慣れてると間違いやすいところです。

ちなみに、リスト・コールチェインの評価についてですが、次のような順番になります。

  • リストは先頭から順に評価する
  • コールチェインは次のように評価する
    1. 暫定的な結果データを決定する
    • コールチェインの先頭の場合、先頭の識別子に紐づくデータを結果とする
    • そうでない場合、5. の結果を使う
    • この後、引数リストの指定がない場合、この結果をそのままコールチェインの結果として評価を終了する
    1. ( 引数リストの指定がある前提で ) 引数のリストを評価する
    2. 引数のリストの各コールチェインの値を渡して1.の関数を呼び出す
    • 呼び出す関数の引数の個数と数が合わない場合、超過分は無視され、不足分は 0 が補われる
    1. 呼び出された時点で関数本体のコード(リスト)を評価する ( 遅延評価 )
    2. 4.のリストの最後のコールチェインの値を結果とし、1. に戻る

ここで補足ですが、カッコ()についてはあくまでリストを囲むための意味しかないため、他の言語であるような「式を部分的にまとめて見易くしたり優先度を調整する」ような使い方はできません。そして、コールチェインの先頭(空白文字除く)は必ず識別子として扱われます。逆に言うと、(,の直後など、識別子がくるべき箇所にカッコ・カンマを書くと識別子として扱われるという、なかなか混乱を招く挙動を見せます。ただこれは言語設計上意図されたものというよりは、実行系での例外処理のケア不足によるバグのようなものでしょう。

後は、golf上地味に重要な点として、コードの最後の閉じカッコ ) は省略可能です。自動的に閉じカッコが必要数補われたものとして実行されます。また、コード先頭の基本関数の割り当てで、もし不要なら、eof(?)の割り当ては省略可能です。( 厳密には省略ではないが、事実上そう見做せる )
これらにより、若干ではありますが文字数が削減できます。

Functional()での変数スコープ

続いて、変数のスコープについての話題です。基本関数のassign(:)とdefine($)がどう違うのか、という話も含みます。

この辺は他の関数型言語や、Ruby等のラムダ式やブロックの入れ子と、それほど感覚的に変わらないと思いますが、Functional() ではユーザ関数定義単位で変数のスコープが作られ、呼び出し元 ( 外側 ) での変数は、そのまま透過的に参照することができます。
ここで、新たに作られたスコープ ( 内側 ) で、define($) を用いることで新たな変数が作られ、これは外側から直接見ることができないものとして扱われます。
※同一スコープ内での2度目以降のdefine($)は、後述するassign(:)と同じく、変数を作らずに紐づきのみを変更します。
また、外側の既存の変数と名前が重複した場合、外側を覆い隠すような扱いとなります。

例えば、次のようなコードを見てみます。

スコープの例
$(v1,値1),
$(f,&()(
  ** ここではv1の値は外側の「その時の」v1の値 → 値4 **
  $(v1,値2),
  ** ここではv1の値は「値2」、外側のv1は変更なし **
  $(v2,値3),
  …
)),
…,
$(v1,値4),
f(),  ← f の内側だけで影響がある
** ここでf内で定義された v1,v2 の影響はない ( v2の値も見えない )**

名前の衝突している v1 は、関数 f の内側でだけ、外側の変数を覆い隠すように働きますし、関数 f の内側で定義された v2 は関数の外側では見えません。
注意が必要なのは、変数のスコープ自体は関数の定義したコード上の入れ子関係で決まるものの、変数を参照した時の値は実行時の評価の順番の影響を受けるということです。
つまり、上のコードで f を定義した時点では、まだ外側の v1 の値は「値1」でしたが、f呼び出し前に define($) により変更を受けていますので、呼び出した f の先頭では「値4」として見えていることになります。

なお、作成した関数を呼び出す場合、function(&)呼び出し時に指定した識別子(変数)に引数がセットされる状態となりますが、これは関数冒頭で define($) しているのと同じ扱いになり、内側のスコープで新しく変数が作られています。
余談ですが、このことを利用すれば、多数の変数を define($) で作る代わりに、ダミーの関数を通じて引数として変数を作る方が、コード量が少なくなることもあります。

define/引数でのコード量違い
** defineの場合 **
$(v1,値1),$(v2,値2),$(v3,値3),主処理

** 引数の場合(コード量少) **
&(v1,v2,v3)(主処理)(値1,値2,値3)  ← 関数を作成し、直ぐに呼び出し

最後に assign(:) ですが、これは現在のスコープで参照している変数の紐づきを直接書き換えます。つまり、新たには変数を作成しません。
「現在のスコープで参照している変数」というのは、現在のスコープの入れ子状況で、最も内側で定義されているものを指します。次に例を挙げます。

スコープの入れ子とassignの影響
$(v0,~),$(v1,~),$(v2,~),$(v3,~),
$(f,&()(
  $(v1,~),  ← f内(g,h含む)で有効なv1で外側のv1をマスク
  $(g,&()(
    $(v2,~), ← g内(h含む)で有効なv2で、外側のv2をマスク
    $(h,&()(
      $(v3,~), ← h内のみ有効なv3で、外側のv3をマスク
      …,
      :(v0,値), ← 最も外側のv0の書き換え
      :(v1,値), ← f内のv1の書き換え
      :(v2,値), ← g内のv2の書き換え
      :(v3,値), ← h内のv3の書き換え ( define($) と同等 )
    )),…
  )),…
)),…

途中のスコープで define($) されていると影響が分かりにくくはなるのですが、このassign(:)は、スコープを越えて変数を共有するような、グローバル変数的な使い方をするために有用です。
なお、コード上一度も define($) されていない変数であっても、システム上は「最も外側のスコープで値 0 として define($) されている」のとほぼ同等の扱いになっていますので、define($) 無しの assign(:) は、トップレベルでの変数変更と同じ効果を持ちます。

Functional()での基本的な実装

では、Functional()の紹介最後の話題として、基本的なコード実装を見てみます。

条件分岐

Functional()では、リストに指定したものは全て評価対象になります。条件選択的な機能を持つ 0,1 であっても、あくまで「引数を評価してからどちらの値を採るか選択する」に過ぎないので、「条件によって評価対象を変える」のであれば、コード本体が遅延評価される関数を使うことになります。
例えば、cond の値 1,0 に応じて、関数 f または g を呼ぶ場合、cond(f(),g()) ではN.G.で、cond(f,g)() のように「呼ぶ関数を先に選択してから関数呼び出し」の形を使います。

もし、f,gのように変数と紐づけがなければ、その場で関数を作ってしまいます。典型的には次のコードのようになります。

cond(
 &()( 条件が真(1)の場合の処理 ),
 &()( 条件が偽(0)の場合の処理 )
)()

もし、if-else型ではなく、ifのみ、あるいはelseのみであれば、「無処理」の関数として 0 を指定すれば済みます。加えて、省略した引数には 0 がセットされますから、ifのみであれば cond f() のようなコードで十分です。

ループ

ループは、他の関数型言語でもよくあるような再帰呼び出しにより実現しますが、繰り返しの制御には条件判断の伴う不定回数のものと、コールチェインの特性を活かした固定回数のものと、主に2通りの実装があります。

  • 不定回数
    前章の条件分岐の応用で、「条件がループ続行なら、自分自身を呼び出す関数を再帰的に呼び出す」という実装を行います。
  • 固定回数
    「自分自身を返り値として返す」ことで、コールチェインにより次々に引数を指定するようにしてループ回数を制御する方式です。特に write(>) によるビットの書き込みの処理は繰り返し実行する使い方になりがちですから、チェインで書き込み対象をつなげられると便利です。

それぞれ次のようなコードになります。

ループ実装例
** 不定回数1( contで継続判定 ) **
$(LOOP1,&()(
  cont()(
   &()( 処理, LOOP1() )
  )()
))
→ LOOP1() でループ実行

** 不定回数2( endで終了判定 ) **
$(LOOP2,&()(
  end()(
   0,
   &()( 処理, LOOP2() )
  )()
))
→ LOOP2() でループ実行

** 固定回数 **
$(LOOP3,& a(
  aを使った処理, LOOP3
))
→ LOOP3 x y z で、x,y,z を対象にループ処理実行

サンプル

では、以上の条件分岐、ループの実装方法を踏まえ、簡単なサンプルを紹介します。
それぞれ、オンライン実行用ページへのリンクも付けています。

  • HelloWorld
    言わずと知れた最初に書かれることが多い、Hello, World!出力コードです。今回は改行無しバージョンです。
    関数Wを、上述の「固定回数のループ」として実行し、各文字のASCIIコードのビットを下位→上位の順に計104ビット分並べることで実装できます。

    素直なHelloWorld(260B)
    0,1,=,:,$,&,<,>,?,
    $(W,& a(> a,W)),
    W 0 0 0 1 0 0 1 0  1 0 1 0 0 1 1 0  0 0 1 1 0 1 1 0  0 0 1 1 0 1 1 0
      1 1 1 1 0 1 1 0  0 0 1 1 0 1 0 0  0 0 0 0 0 1 0 0  1 1 1 0 1 0 1 0
      1 1 1 1 0 1 1 0  0 1 0 0 1 1 1 0  0 0 1 1 0 1 1 0  0 0 1 0 0 1 1 0
      1 0 0 0 0 1 0 0
    

    hakerh400.github.ioでのオンライン実行
    簡明な実装だけではgolfとしては物足りないでしょうから、縮めたバージョンも載せておきます。( コードの構成については割愛します )

    139B版HelloWorld
    0,1,=,:,$,&,<,>,$(W,& a(>(a(:(Z,&()(W()0)))),W))0 Z Z()1()1 W()W()>()W()>()> >()>()W()1()0()0()Z W 1()1()1()> >()> Z W 1()W()>()Z W()1()0 Z
    

    hakerh400.github.ioでのオンライン実行

  • cat
    もう1つのサンプルとして、catコマンド相当 ( 標準入力から読み込んだデータをそのまま標準出力に出力する ) のコードです。
    こちらは必然的に不定回数のループによる実装になります。eof(?)はそのままループ終了条件判定、ループ内のメインの処理は、読み込んだビットをそのまま書き込む >(<()) です。

    cat実装
    0,1,=,:,$,&,<,>,?,
    $(L,&()(
      ?()(
       0,
       &()(
        >(<()),L()
       )
      )()
    ))()
    

    hackerh400.github.ioでのオンライン実行

FizzBuzz実装

それではいよいよ、今回のテーマであるFizzBuzzの実装について解説します。

コード全容

こちらが273B版コードになります。文字数削減のため、改行を含め無駄な空白文字は無くし、eof(?)の割り当てや末尾の閉じカッコ)も無くした状態です。

273B版コード
0,1,=,:,$,&,<,>,$(X,& f($(I,T(f(a,b,c,d),b d(0,I)(= a,a(=,1)b,a b(=,1)c,a b(c,d))))1))(:(T,&(a,b,c,d))(X(T(b(d,a c)($(S,T(W()1 b()0()1()1 0(= b)b(= b)>()0 1()> >()0 1()> > 0,0)),=(:(t,t(S,1)))(0,&()(u()>()0,:(W,T(>(a 1),W))a b c d >()0)))0,W()1()1()0()0)),:(u,&()(W a b c d

hackerh400.github.ioでのオンライン実行

流石にそれでは見辛いので、次が整形したバージョンになります。末尾の閉じカッコも補完しています。

整形版(末尾に閉じカッコ補完)
0,1,=,:,$,&,<,>,
$(X,& f(
  $(I,T(
     f(a,b,c,d),
     b d (0,I) (= a,a(=,1) b,a b (=,1) c,a b (c,d))
  )) 1
))(
 :(T,&(a,b,c,d))(
  X(
   T(
    b(d,a c)(
     $(S,T(
       W() 1 b () 0 () 1 () 1 0 (= b) b (= b) > () 0 1 () > > () 0 1 () > > 0,
       0
     )),
     =(
      :(t,t(S,1))
     )(
      0,
      &()(
       u() > () 0,
       :(W,T(>(a 1),W)) a b c d > () 0
      )
     )
    ) 0,
    W() 1 () 1 () 0 () 0
   )
  ),
  :(u,&()(W a b c d))
 )
)

FizzBuzzコード基本構成

今回のコードの基本構成ですが、鍵は「いかに100回のループを捌くか」という点にあります。これを、次のPython疑似コードのような "executer" 関数 X を設けて、1~10のループ×2 に分解するというアイデアが基本となります。

1~10ループ用executer疑似コード
def executer(f):
  def incrementer(a,b=0,c=0,d=0):
    f(a,b,c,d)
    nextf = nullf if b==1 and d==1 else incrementer
    # nullf is a function to do nothing
    nextf(1^a,a^b,(a&b)^c,(a&b&c)|d)

  incrementer(1)

この executer は、ループ内の処理を具体的に行う f を引数として、1~10 の4ビット表現に対応した(a,b,c,d)の組をインクリメントしていくものです。( aが最下位ビット )
インクリメント処理は、(a,b,c,d)→(1^a,a^b,(a&b)^c),(a&b&c)|d) ( &,|,^ はビットand,or,xor ) により実現でき、終了判定は10の時に対応する b==1 and d==1 で賄えますから、これをFunctional()の0,1,=を組み合わせて次のコード部分で実装しています。
内部で定義している関数 I は、ちょうど不定回数のループの実装になっていて、それを直ぐに初期パラメータ a=1 で呼び出しています。( do-until型なので、上で紹介したものと少し形が違うことに注意 )

executer関数X実装
$(X,& f(
  $(I,T(
     f(a,b,c,d),
     b d (0,I) (= a,a(=,1) b,a b (=,1) c,a b (c,d))
  )) 1
))

なお、T は後で assign される箇所がありますが、&(a,b,c,d) により作成された4引数関数用の関数テンプレートを紐づけた変数です。これを使いまわすことで、関数作成部分を短縮しています。

これら X,T により、コード全体はおおまかに次のように構成しています。

FizzBuzz全体構成
X(
 T(
  X(
   T(
    ** 1の位に応じたメインの処理
    * Fizz/Buzz/FizzBuzz/数字の出力
    * 改行の出力
   )
  ),
  ** 10の位切り替わり時の後処理
  * ループ次周での上位桁出力用に u の内容を更新
 )
)

つまり、内側の X,T で1の位に応じたループを構成し、それと10の位切り替わり時の後処理を組み合わせたものを、外側の X,T でループにするという2重ループ構造です。
なお、同じ T を使って関数を作成してますので、どちらも a,b,c,d の4変数で1~10を表していますが、もちろんコードの箇所によって、1の位に対応した数値か、10の位に対応した数値かは変わってきます。

定義した関数

前項で X,T について説明しましたが、他に定義した固定処理用の関数について説明します。

  • ビットストリーム出力関数 W
    コードの途中で :(W,T(>(a 1),W)) として定義しているのが該当します。ここでも T を使いまわしており、4変数関数として定義されていますが、実際には第一引数の a しか使いません。
    「Functional()での基本的な実装」の「サンプル」の章で挙げたHelloWorldの W に似ていますが、少しひねりを入れて、少ないデータで同等の処理ができるように工夫しています。
    W 0,W 1では、HelloWorldの時と同じようにビット0,1を出力しますが、W > と、> を指定することでビット1×2を出力できるというのがその工夫です。これにより、例えば 1の位 (a,b,c,d) に対応した出力が W a b c d 1 1 0 0 の代わりに W a b c d > 0 0 と短縮できます。
    ※引数 0 は、空リスト () でも代替できるので、コード上はassignも併せて :(W,T(>(a 1),W))a b c d >()0 のようにチェインして、細かく短縮しています。
  • Fizz/Buzz出力関数S
    上述の W を用いて、Fizz あるいは Buzz を出力する関数です。やはりこれも T を使いまわして4変数関数として作成しているのですが、Fizz か Buzz かの切り替えは第二引数 b で行います。( b=0→Buzz, b=1→Fizz )
    Fizz/Buzz出力関数S定義部
       $(S,T(
         W() 1 b () 0 () 1 () 1 0 (= b) b (= b) > () 0 1 () > > () 0 1 () > > 0,
         0
       )),
    
    Fizz,Buzzはともに4文字32ビット分の書き込みになりますが、ビットとしては4か所分しか違いがありませんから、まとめてしまった方が効率が良いことになります。
    なお、書き込み後に ,0 をつけることで、明示的に 0 を返す ( 付けなければ W が返る ) ようにしているのですが、その意図は次の章で説明します。

可変処理の制御

最後に、Fizz/Buzz/FizzBuzz/数字の切り替えの肝の部分と、それに関わる変数 u,t についての説明となります。

  • 上位桁出力切り替え関数 u
    先に、上位桁出力を切り替えるための関数 u についてです。
    1回目の外側ループ処理時、u は未定義状態 ( 値としては 0 ) なので、上位桁の出力はなんら行われません。これが1~10の時の処理に丁度都合よくマッチします。それを、外側ループ最後に :(u,&()(W a b c d)) と下位4ビットを出力する関数としてassignすることで、次の周の上位桁出力に使えるようにします。
    ※このため、上位桁のループも下位桁と同じように1~10で回すことができます。
    なお、u は W を返しますので、上位4ビットの出力は内側のループで u()>()0 の形で賄っています。( u 自身にまとめるより1文字縮む )
  • Buzz/数字出力切り替え
    Buzzの判定は下位桁の4ビットを見るだけで済みますので、Buzz か数字どちらを出力するかは、b(d,a c)( S, Fizz判定(0,数字出力処理関数) ) 0 と、if文の応用として実装できます。
    ※b(2の桁)が立っていれば、d(8の桁)の有無で10と分かり、bが立っていなければ、a,c(1,4の桁)のandを取って5かどうかが分かる。
    条件が真になれば S が選ばれ、引数 0 が渡って Buzz が出力されるという寸法です。
    Fizz判定については次項で説明します。数値出力処理関数は、前述の W,u を使って無名の関数として作成しています。
  • Fizz出力用3状態変数 t
    3の倍数判定と Fizz の出力は、3状態をとる変数 t で制御します。この変数は assign での書き換えによりグローバル変数的に用います。
    3状態の切り替えは :(t,t(S,1)) だけで済むのですが、これにより t=0 → t=1 に遷移、t=1 → t=S に遷移、t=S → S呼び出しによりFizzを出力した上で t=0 に遷移という制御を実現しています。なお、前章で S が 0 を返すようにしていると説明したのは、この t=0 への遷移のためです。
    この3状態切り替え処理は、上のBuzz/数字出力切り替えでのFizz判定も兼ねています。Fizz出力後のみ t=0、その他は非0となりますので、=( :(t,t(S,1)) ) と全体を = に渡して not にかけることで、そのままFizzを出力したかどうかの判定になるのです。もちろん、判定よりもFizz出力が先に行われますので、15の倍数でのFizzBuzzに対してのケアは不要というエコ設計です。

このようにして制御することで、FizzBuzz実装に必要な条件判断を乗り切っているということです。

269bytesへの短縮(2022/12/31追加)

本記事投稿後、短縮できるポイントがあったことに気付き、更に4bytes記録を更新しました。
実は、12/6時点でもう更新はしていたのですが、他の記事との兼ね合いもあって解説を書くのが先延ばしになっていたような状況でした。

では、269bytes版コードですが、次のようになります。

269B版コード
0,1,=,:,$,&,$(X,& >($(I,T(>(a,b,c,d),b d(0,I)(= a,a(=,1)b,a b(=,1)c,a b(c,d))))1))(:(T,&(a,b,c,d))(X(T(b(d,a c)($(S,T(W()1 b()0()1()1 0(= b)b(= b)>()0 1()> >()0 1()> > 0,0)),=(:(t,t(S,1)))(0,&()(u()>()0,:(W,T(>(a 1),W))a b c d >()0)))0,W()1()1()0()0)),:(u,&()(W a b c d

hackerh400.github.ioでのオンライン実行

なお、273bytes版との相違は、次のdiffのように、先頭の僅かな部分でしかありません。

273B版とのdiff(整形版のコードで比較)
@@ -1,7 +1,7 @@
-0,1,=,:,$,&,<,>,
-$(X,& f(
+0,1,=,:,$,&,
+$(X,& >(
   $(I,T(
-    f(a,b,c,d),
+    >(a,b,c,d),
     b d (0,I)(= a,a(=,1)b,a b(=,1)c,a b(c,d))
   )) 1
 ))(

具体的には、1行目の <,>, の4文字を削った上で、関数 X の仮引数として使用している f> に替えた ( 2,4行目 ) という点になります。
別記事「Functional()版Quine 602bytes実装」でも書きましたが、最初に基本関数の割り当てをずらずら列挙せずにオーバーラップさせて文字数を節約することができる場合があり、このコードでも ( < の出番がないため ) & 以降をオーバラップできたということで、4bytesの短縮につながりました。

おわりに

言語自体の紹介とFizzBuzzの解説とで長くなりましたが、こちらでFunctional()実装のイメージは大分つくのではないかと思います。紹介での繰り返しになりますが、Brainf**kと並ぶシンプルな言語仕様なので、初心者にもお勧めできるものと思います。この機会に是非いかがでしょうか。
なお、冒頭で紹介した第7回Esolang Codegolf Contest Writeupで、その時の問題を解くコードとして220B版をあげましたが、今改めて組んでみたところ、hakerh400.github.ioでのオンライン実行のように194Bに縮みました。参考までに載せておきます。

Discussion