Esolang CodeGolf Contest #8 番外編 Functional()解
はじめに
かなり今更になりますが、2023/7/15~17の3日間、TSG/KMC開催のesolangを対象にしたコードゴルフコンテストに参加させて頂きました。以下のtweet(X-post)のように、陣取り合戦形式になっていて、占領済み陣地からゴルフスコアで上回ることにより陣地を拡大していくように競い合うものです。
詳しくは、第8回Esolang Codegolf Contest Writeupをご覧ください。
「本編はどこなのか?」と言うと、特に記事は作ってないのですが、今回コンテスト対象でなかった ( 以前のコンテストで対象だったため修得していた ) 言語、Functional() の供養のため、いきなり番外編ということで解説したいと思います。
※Functional()の概要については、Functional() (esolang)の紹介とFizzBuzz 273bytes 実装(後日269に更新)をご覧ください。
解法
ということで、以下の 326 Bytes の解を実装しました。( 末尾に改行は入りません )
0,1,=,:,$,&,<,>,?,&(R,J)($(W,T(> a,W)),$(X,T(= a(1,&()(=(J(0,a)b c d e f)(0,X(b,c,d,e,f,a))))1)),$(L,T(?(T(W 0(J(1,a)c e g(R()))1()0()1 0(X(b,d,f,h,R()))1()0 1()1()0(R())()1()0()0)(R(),R(),R(),R(),R(),R(),R(),R()))(0,L)()))())($(T,&(a,b,c,d,e,f,g,h))(T(a(b(<,<()(?,$,<(<(<(<(<())))))),b(&,>)))(<(),<())),T($(F,& x(=(a,=(b,x))F
解説
方針
お題については、上記Write-upをどうぞ。
ざっくり言うと、1行あたり A1B2C1D3B4
のように「スート(A~D)+数字(1~5)×5組」で表されるトランプの手札を見て、フラッシュ(スートの一致)・ストレート(1~5の連番)の成立をそれぞれ判定し、出力するものです。
各言語でのアプローチには正規表現や計算的な工夫も含めて様々あるのですが、Functional() は本当に最小限の命令セットしか持っておらず、数値演算も文字列処理も素ではままならないため、地道に比較を行うしかありません。
出力に関しては、フラッシュ成立なら F、ストレート成立なら S を出力行に含める ( 両方不成立でも改行は必須 ) となっていますが、無駄な文字を含めても良いため、成立・不成立で 1bit分だけ反転させて、フラッシュ成立/不成立なら F/D を、ストレート成立/不成立なら S/R を出力するということで、出力処理を統一して単純化します。
整形コードと主要処理
さて、では元のコードでは読み辛いので、インデントを入れて整形し、省略していた末尾の閉じカッコを補ったコードを以下に示します。
0,1,=,:,$,&,<,>,?,
&(R,J)(
$(W,T(> a,W)),
$(X,T(
= a(
1,
&()(
=(J(0,a)b c d e f)(
0,
X(b,c,d,e,f,a)
)
)
)1
)),
$(L,T(
?(
T(
W 0(J(1,a)c e g(R()))1()0()1 0(X(b,d,f,h,R()))1()0 1()1()0(R())()1()0()0
)(
R(),R(),R(),R(),R(),R(),R(),R()
)
)(0,L)()
))()
)(
$(T,&(a,b,c,d,e,f,g,h))(
T(
a(
b(
<,
<()(
?,$,<(<(<(<(<()))))
)
),
b(&,>)
)
)(
<(),<()
)
),
T(
$(F,& x(
=(a,=(b,x))F
))
)
)
変数名と関数本体の紐づけが2パターンあるので分かり辛いと思いますが、R
, J
が引数に関数本体を指定する形で ( 本体は最後の $(T,~)(~)
と T($(F,~))
)、W
,X
,L
が本体処理内部で直接 $
によって定義されています。
※できれば全て「引数に関数本体を指定する形」にしたいのですが、自己参照を伴う関数はそれができないため、直接の定義にしているのです。
それぞれの役割は次の通りです。
- R: 1文字読み込んで、文字に応じて5種類のオブジェクトを返す関数
- J: 複数のオブジェクトの全一致あるいは全不一致を判定する関数 ( フラッシュ判定を兼ねる )
- W: ビットストリームを出力する関数
- X: ストレート判定用関数
- L: 行単位の処理をEOFまで繰り返すメインループ
なお、関数の作成は &
を使って &(a,b,c)(処理)
のように書くのですが、この &(a,b,c)
の部分を繰り返すのは文字数の無駄なので、変数 T
に最大8変数 ( a
~h
) 対応可能なテンプレートを定義して、それを使いまわします。
※関数を入れ子にして定義すると、引数の文字がバッティングするため、それで困る場面だけ直接 &
を使います。
最後の L
を使って、出力関数 W
に 0A100010,B1001010,0C010000 とビット列を渡して出力することによって、F/D,S/R,改行の3文字の出力を実現しています。この W
ははじめにで紹介した概要記事にある出力処理と同じものです。
※ビット列は下位優先、Aはフラッシュ判定に応じた1/0、Bはストレート判定に応じた1/0、Cは1で固定
各処理の詳細
1文字読み込み(R)
この処理では、<()
8回で1バイト読み込みつつ、最初の3回分 ( 下位3ビット ) に応じて5種類のオブジェクトを返す処理です。
オブジェクトは 0 以外で、5種類区別できればなんでも良いので、割り当て済みの $
,<
,>
,&
,?
を使います。Rubyでの疑似コードはこんな感じです。
fR = ->{
x = $stdin.getbyte
return case x%8
when 3; return '<' # 3 or C
when 5; return '?' # 5
when 1; return '$' # 1 or A
when 4; return '&' # 4 or D
when 2; return '>' # 2 or B
end
}
一致・不一致判定(J)
この処理では、フラッシュやストレートの判定の元となる「全てが一致」「全てが不一致」を実現するための処理です。とは言え、あくまで最初の様子を基準にして「全て基準と一致」「全て基準と不一致」とするだけなので、フラッシュ判定には良いですが、ストレート判定にはもう一段階工夫が必要です。
この処理のRubyでの疑似コードは次の通りです。
NULLFUNC = ->x { return NULLFUNC } # variable 0
fJ = ->op,base{
fF = ->x{
return (x==base)^op ? NULLFUNC : tF # Truthy if tF, Falsy if NULLFUNC
}
return fF
}
このJ
を使って、J(1,a)(b)(c)(d)(e)
とすれば、a
~e
の全一致を ( 全一致なら Truthy, 不一致なら 0 )、J(0,a)(b)(c)(d)(e)
とすれば、b
~e
が全部a
と不一致であることを ( 全不一致なら Truthy, 一致があるなら 0 ) 判定できます。
ストレート判定(X)
前述のJ
を使ってストレート判定を行うものです。
端的には、J(0,a)(b)(c)(d)(e)
, J(0,b)(c)(d)(e)(a)
, J(0,c)(d)(e)(a)(b)
, J(0,d)(e)(a)(b)(c)
を繰り返し行って全部 Truthy ならストレートだと、そういう判定が行えれば良いわけで、番兵 0 を引数 f
に追加して循環的に再帰を行います。
Rubyでの疑似コードは次のような感じになります。
IDFUNC = ->x { return x } # variable 1
fX = ->a,b,c,d,e,f{
if !a
func = IDFUNC
else
func ->_{
# NULLFUNC, fJ は Jの疑似コードで出ていたものと同一
if fJ(false,a)(b)(c)(d)(e)(f) == NULLFUNC
return NULLFUNC
else
return fX(b,c,d,e,f,a)
end
}
end
return func(true)
}
おわりに
ということで、Functional()での実装を解説してみました。
「全てのオブジェクトが関数」という点に慣れれば、機能はとても単純なのでgolfするのに敷居が低い言語ではないかと思います。
Discussion