0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

変数を使わずに 99 bottles of beer

Last updated at Posted at 2025-03-05

はじめに

J 言語では、なるべく変数を使わないようなプログラムの書き方ができます1。例えば、次の 99 bottles of beer を出力するプログラムは全く変数を使わずに書いてあります。

(' of beer on the wall.',~'no more'"_`":@.*,' bottles'}.~-@=&1)((('Go to the store and buy some more, ',@)99)`(10{a.,~('Take one down and pass it around, ',@))@.*(]&(0 0$1!:2&2))((1&(3!:12)@{.0}}:,', ',_13&}.,{:)@[.))<:"+i._100

もちろん、これを実行するときちんと動作します。(ただし、実行にはJ904以降のバージョンが必要です2。)

出力 (長いので一部省略)
99 bottles of beer on the wall, 99 bottles of beer.
Take one down and pass it around, 98 bottles of beer on the wall.

98 bottles of beer on the wall, 98 bottles of beer.
Take one down and pass it around, 97 bottles of beer on the wall.

97 bottles of beer on the wall, 97 bottles of beer.
Take one down and pass it around, 96 bottles of beer on the wall.

...

2 bottles of beer on the wall, 2 bottles of beer.
Take one down and pass it around, 1 bottle of beer on the wall.

1 bottle of beer on the wall, 1 bottle of beer.
Take one down and pass it around, no more bottles of beer on the wall.

No more bottles of beer on the wall, no more bottles of beer.
Go to the store and buy some more, 99 bottles of beer on the wall.

変数を取り除いていく過程をお見せしつつ、J の面白さを少しでも伝えられればと思います。

作り方

まず、先程のプログラムを再掲します。

(' of beer on the wall.',~'no more'"_`":@.*,' bottles'}.~-@=&1)((('Go to the store and buy some more, ',@)99)`(10{a.,~('Take one down and pass it around, ',@))@.*(]&(0 0$1!:2&2))((1&(3!:12)@{.0}}:,', ',_13&}.,{:)@[.))<:"+i._100

これだけ見せられても意味がわからないと思うので、適当に verb (関数のこと) に切り分けた状態から見ていきましょう。{{ }} で囲ってあるのが verb の定義で、引数には変数 y でアクセスできます3

count=. {{
   bottles=. (('no more'"_)`":@.* y) , ((-(y = 1)) }. ' bottles')
   bottles , ' of beer on the wall.'
}}

current=. {{
   s=. count y
   head=. toupper {.s
   rest=. (}:s) , ', ' , (_13 }. s) , ({:s)
   head 0} rest
}}

take=. {{ ('Take one down and pass it around, ' , (count <:y)) , LF }}
buy=. {{ 'Go to the store and buy some more, ' , (count 99) }}
action=. buy`take@.*

output=. {{
   echo current y
   echo action y
}}
output"+ i._100

とても見通しが良くなりましたね。見慣れない記号の羅列もあるかもしれませんが、NuVoc に一覧があるのでそれを参照しつつ読み進めていただければと思います。

プログラムの詳細

さて、count から見ていこうと思います。これは数値を受け取って 'N bottle(s) of beer on the wall.' の形の文字列を返す verb です。

count=. {{
   bottles=. (('no more'"_)`":@.* y) , ((-(y = 1)) }. ' bottles')
   bottles , ' of beer on the wall.'
}}

J では基本的に右側の verb から順に実行されていくので、右側から見ていきましょう。verb は引数を 1 つだけ渡す (monad; 例 : f 1) か、2 つ渡す (dyad; 例 : 0 f 'a') かの方法で呼ばれます。+ のような記号の場合も f のような名前の場合も扱いに差はありません。

まず、{. の実行です。y が 1 のときは、_1 }. ' bottles'4 となって、後ろから 1 文字を除いた文字列 ' bottle'が返ります。1 以外の場合は、除かれるのが 0 文字になるので ' bottles' のままです。

次に、前半部分です。('no more'"_)`": は、引数を無視して 'no more' を返す verb と、引数を文字列にしたものを返す verb ": の並び (gerund) を作ります。@.* で、その gerund の *y 番目の verb を y に適用します。 * はここでは単項演算子 (monad) として使われているので、掛け算ではなく符号 (_1, 0, 1) が返ります。結果として、y が 0 のときだけ 'no more' に替えた文字列となります。

続いて、current です。この verb は count の結果を用いて 'N bottle(s) of beer on the wall, N bottle(s) of beer.' の形の文字列を作ります。

current=. {{
   s=. count y
   head=. toupper {.s
   rest=. (}:s) , ', ' , (_13 }. s) , ({:s)
   head 0} rest
}}

head には、s の先頭の文字を大文字化した文字が入ります (大文字にするのは行頭で 'No more' とするため)。

rest は、s の末尾のピリオドを落とした文字列、コンマ、s から末尾 13 文字 ' on the wall.' を除いた文字列、s の末尾にあるピリオドを連結します。

最後に 0}rest の 0 番目 (先頭) の文字を head に替えた文字列を返します。

残りは より簡単なので、サクッと見ていきましょう。

take=. {{ ('Take one down and pass it around, ' , (count <:y)) , LF }}
buy=. {{ 'Go to the store and buy some more, ' , (count 99) }}
action=. buy`take@.*

<:yy より 1 小さい数を返します。また、take で最後に改行を加えているのは出力の段落間に空行を入れるためです。

@.* は先程も登場しましたが、0 の場合とそうでない場合とで分岐を行っています。

最後に、実行部分です。

output=. {{
   echo current y
   echo action y
}}
output"0 i._100

J では verb は常に右結合5なので、echo current yecho (current y) と同じになります。

i._100 は、99 から 0 までの 100 個の整数が降順に並んだ配列を返します (正の数を指定すると昇順の配列になります)。output"0 は、その配列の一つ一つの値 (atom) について output を適用します6

以上がプログラムの説明です。記号が多くて見た目にクセがあるだけで、とりわけ難しい言語ではないと おわかりいただけましたでしょうか。

引数名の除去

ここからが J の一番楽しい部分です。J には いくつかの道具があって、上手く使えば引数を書かずに verb を定義することができます。一部を以下に挙げます。

  • ~ (reflex, passive) : 引数の複製、入れ替え
    • (u~) yy u y
    • x (u~) yy u x
  • @ (atop), & (compose) : 関数合成7
    • (u@v) yu (v y)
    • x (u@v) yu (x v y)
    • (u&v) yu (v y)
    • x (u&v) y(v x) u (v y)
  • & (bond) : 部分適用
    • (m&v) ym v y
    • (u&n) yy u n
  • fork
    • (w u v) y(w y) u (v y)
    • x (w u v) y(x w y) u (x v y)
    • (m u v) yn u (v y)
    • x (m u v) ym u (x v y)
  • hook
    • (u v) yy u (v y)
    • x (u v) yx u (v y)

ここで、u, v, w は verb を表し、m, n は noun (文字列や数値などの値) を表します。

~@ などは modifier に分類され、verb との違いとして verb を引数にとることができます。そのうち ~ のように引数が 1 つのものは adverb、@ のように引数が 2 つのものは conjunction と呼びます。これまでに出てきた } は adverb で、`@. は conjunction です。

特筆すべきは fork と hook で、verb を並べて書くだけで単なる関数合成より複雑な verb を作ることができます。4 つ以上の verb の並びの場合は fork や hook の組み合わせとして処理され、例えば a b c d e(a b (c d e))a b c d e f(a (b c (d e f))) となります。ここでも verb は右から見るというルールが適用されています。

一方で、modifier は左結合です。また、modifier は verb よりも高い優先順位をもちます。x 1&+@- yx ((1&+)@-) y、つまり 1 + (x - y) となります。

それでは、count から書き換えていきましょう。

count=. {{
   bottles=. (('no more'"_)`":@.* y) , ((-(y = 1)) }. ' bottles')
   bottles , ' of beer on the wall.'
}}

NB. }. の引数を入れ替え
count=. {{
   bottles=. (('no more'"_)`":@.* y) , (' bottles' }.~ (-(y = 1)))
   bottles , ' of beer on the wall.'
}}

NB. - と = の合成
count=. {{
   bottles=. (('no more'"_)`":@.* y) , (' bottles' }.~ (y -@= 1))
   bottles , ' of beer on the wall.'
}}

NB. -@= を 1 に部分適用
count=. {{
   bottles=. (('no more'"_)`":@.* y) , (' bottles' }.~ (-@=&1 y))
   bottles , ' of beer on the wall.'
}}

NB. fork (m u v)
count=. {{
   bottles=. (('no more'"_)`":@.* y) , ((' bottles' }.~ -@=&1) y)
   bottles , ' of beer on the wall.'
}}

NB. fork (w u v)
count=. {{
   bottles=. ( (('no more'"_)`":@.*) , (' bottles' }.~ -@=&1) ) y
   bottles , ' of beer on the wall.'
}}

NB. 変数 bottles をインライン化
count=. {{
   (( (('no more'"_)`":@.*) , (' bottles' }.~ -@=&1) ) y) , ' of beer on the wall.'
}}

NB. 外側の , の引数を入れ替え
count=. {{
   ' of beer on the wall.' ,~ (( (('no more'"_)`":@.*) , (' bottles' }.~ -@=&1) ) y)
}}

NB. fork (m u v)
count=. {{
   ( ' of beer on the wall.' ,~ ((('no more'"_)`":@.*) , (' bottles' }.~ -@=&1)) ) y
}}

NB. {{ u y }} の形になったので u と書ける
count=. ' of beer on the wall.' ,~ ((('no more'"_)`":@.*) , (' bottles' }.~ -@=&1))

NB. 余分な括弧を除く
count=. ' of beer on the wall.' ,~ 'no more'"_`":@.* , ' bottles' }.~ -@=&1

このように、少しずつ引数を外に追いやることで取り除くことができます。

current も同様に変換していきます。慣れないうちは少し難しいかもしれませんが、順を追って辿っていけば理解できるはずです。

current=. {{
   s=. count y
   head=. toupper {.s
   rest=. (}:s) , ', ' , (_13 }. s) , ({:s)
   head 0} rest
}}

NB. 関数合成で count を外に
current=. {{
   head=. toupper {.y
   rest=. (}:y) , ', ' , (_13 }. y) , ({:y)
   head 0} rest
}}@count

NB. toupper と {. の合成
NB. _13 }. の部分適用
current=. {{
   head=. toupper@{. y
   rest=. (}:y) , ', ' , (_13&}. y) , ({:y)
   head 0} rest
}}@count

NB. rest を fork に
current=. {{
   head=. toupper@{. y
   rest=. (}: , ', ' , _13&}. , {:) y
   head 0} rest
}}@count

NB. 変数をインライン化
current=. {{
   (toupper@{. y) 0} ((}: , ', ' , _13&}. , {:) y)
}}@count

NB. fork
current=. {{
   (toupper@{. 0} (}: , ', ' , _13&}. , {:)) y
}}@count

NB. {{ }} を外す
current=. (toupper@{. 0} }: , ', ' , _13&}. , {:)@count

ここまで来れば takebuy は簡単ですね。

take=. {{ ('Take one down and pass it around, ' , (count <:y)) , LF }}
buy=. {{ 'Go to the store and buy some more, ' , (count 99) }}

take=. LF ,~ 'Take one down and pass it around, ' , count@<:
buy=. 'Go to the store and buy some more, ' , count@99

output は 2 つの文から成るので一捻り加える必要があります。右側の引数を返す verb ] を用いて組み合わせることにします (echo は空8を返すので、結果の値に意味はありませんが)。verb は必ず右から実行されることに注意してください。

output=. {{
   echo current y
   echo action y
}}

NB. ] で組み合わせる
output=. {{ (echo action y) ] (echo current y) }}

NB. & で合成
output=. {{ (action y) ]&echo (current y) }}

NB. fork
output=. action ]&echo current

結果的に、プログラム全体は次のようになりました。J に慣れている人であれば、この形が一番読みやすいかもしれません。

count=. ' of beer on the wall.' ,~ 'no more'"_`":@.* , ' bottles' }.~ -@=&1

current=. (toupper@{. 0} }: , ', ' , _13&}. , {:)@count

take=. LF ,~ 'Take one down and pass it around, ' , count@<:
buy=. 'Go to the store and buy some more, ' , count@99
action=. buy`take@.*

output=. action ]&echo current
output"0 i._100

count 以外の変数は一度ずつしか使われていないので簡単に取り除けますが、count は 3 箇所で使われているので一筋縄ではいきません。定義をコピペするのは望ましくないですし、ひとまず count は そのままにして組み合わせてみましょう。

(
   (
      'Go to the store and buy some more, ' , count@99
   )`(
      LF ,~ 'Take one down and pass it around, ' , count@<:
   )@.*
   ]&echo
   (toupper@{. 0} }: , ', ' , _13&}. , {:)@count =. NB. ここから閉じ括弧までが count の定義
      ' of beer on the wall.' ,~ 'no more'"_`":@.* , ' bottles' }.~ -@=&1
)"0
i._100

J では (今のところ9) 式の途中で改行できないので、改行は取り除いてやる必要があります。ついでに count を短い変数名 f に置き換えたり、"0 を同じ意味の "+ に変えて後続の空白文字を削ったりすると以下のようになりました。207 bytes です。

(('Go to the store and buy some more, ',f@99)`(LF,~'Take one down and pass it around, ',f@<:)@.*]&echo(toupper@{.0}}:,', ',_13&}.,{:)@f=.' of beer on the wall.',~'no more'"_`":@.*,' bottles'}.~-@=&1)"+i._100

完全に変数を無くす

ワンライナーで書けたのは嬉しいですが、結局変数 f が残ってしまうのは ちょっと物足りないですよね。というわけで、完全にゼロにするところまでいきたいと思います。

先程使った fork や hook は verb を作る機能でしたが、modifier についても同じような機能が存在します (ModifierTrains)10。これを利用してどうにかできると嬉しいですね。

f=. ' of beer on the wall.' ,~ 'no more'"_`":@.* , ' bottles' }.~ -@=&1

(
   (
      'Go to the store and buy some more, ' , f@99
   )`(
      'Take one down and pass it around, ' , LF ,~ f@<:
   )@.*
   ]&echo
   (toupper@{. 0} }: , ', ' , _13&}. , {:)@f
)"+
i._100

この式から、f を外に掃き出していくことを考えます。

(
   (
      'Go to the store and buy some more, ' , f@99
   )`(
      LF ,~ 'Take one down and pass it around, ' , f@<:
   )@.*
   ]&echo
   (toupper@{. 0} }: , ', ' , _13&}. , {:)@f
)

NB. u (N0 V1 C2) n → N0 V1 (u C2 n)
(
   (
      f('Go to the store and buy some more, ' , @)99
   )`(
      LF ,~ 'Take one down and pass it around, ' , f@<:
   )@.*
   ]&echo
   (toupper@{. 0} }: , ', ' , _13&}. , {:)@f
)

NB. u (N0 V1 C2) v → N0 V1 (u C2 v)
(
   (
      f('Go to the store and buy some more, ' , @)99
   )`(
      LF ,~ f('Take one down and pass it around, ' , @)<:
   )@.*
   ]&echo
   (toupper@{. 0} }: , ', ' , _13&}. , {:)@f
)

NB. u (N0 V1 C2) v → N0 V1 (u C2 v)
(
   (
      f('Go to the store and buy some more, ' , @)99
   )`(
      f(LF ,~ ('Take one down and pass it around, ' , @))<:
   )@.*
   ]&echo
   (toupper@{. 0} }: , ', ' , _13&}. , {:)@f
)

NB. u (A0 C1 C2) v → (u A0) C1 (u C2 v)
(
   f(
      (('Go to the store and buy some more, ' , @)99)
      `
      (LF ,~ ('Take one down and pass it around, ' , @))
   )<: @.*
   ]&echo
   (toupper@{. 0} }: , ', ' , _13&}. , {:)@f
)

NB. u (C0 C1 V2) v → (u C0 v) C1 V2
(
   f(
      (('Go to the store and buy some more, ' , @)99)
      `
      (LF ,~ ('Take one down and pass it around, ' , @))
      @.*
   )<:
   ]&echo
   (toupper@{. 0} }: , ', ' , _13&}. , {:)@f
)

NB. u (V0 C1 C2) v → V0 C1 (u C2 v)
NB. [. は左側の引数を返す conjunction
(
   f(
      (('Go to the store and buy some more, ' , @)99)
      `
      (LF ,~ ('Take one down and pass it around, ' , @))
      @.*
   )<:
   ]&echo
   f((toupper@{. 0} }: , ', ' , _13&}. , {:)@[.)<:
)

NB. u (C0 C1 C2) v → (u C0 v) C1 (u C2 v)
NB. 両方に f と <: を持ってきたのでまとめられる
f(
   (
      (('Go to the store and buy some more, ' , @)99)
      `
      (LF ,~ ('Take one down and pass it around, ' , @))
      @.*
   )
   (]&echo)
   ((toupper@{. 0} }: , ', ' , _13&}. , {:)@[.)
)<:

NB. 余分な括弧を除く
NB. modifier train は左結合
f(
   (('Go to the store and buy some more, ' , @)99)
   `
   (LF ,~ ('Take one down and pass it around, ' , @))
   @.*
   (]&echo)
   ((toupper@{. 0} }: , ', ' , _13&}. , {:)@[.)
)<:

はい。というわけで、めでたく括弧の中が 1 つの conjunction になって、f を 1 箇所にまとめることができました。

これを用いると、以下のように書くことができます。

(' of beer on the wall.' ,~ 'no more'"_`":@.* , ' bottles' }.~ -@=&1)
(
   (('Go to the store and buy some more, ' , @)99)
   `
   (LF ,~ ('Take one down and pass it around, ' , @))
   @.*
   (]&echo)
   ((toupper@{. 0} }: , ', ' , _13&}. , {:)@[.)
)<:"+
i._100

随分すっきりとしていますね。これであとは改行を無くせば完成……でしょうか。

まだ残っている

よく見ると、まだ LF, echo, toupper が残っていますよね。これらは特殊なキーワードではなく、単に標準ライブラリで定義された名前です。これでは完全とは言えません。

LF は (UTF-8 で) 0x0a ですが、J の文字列リテラルには改行や任意の文字に対するエスケープシーケンスがありません。代わりに、0x00 から 0xff までの全ての値からなるバイト列 a. が用意されています1110 {. a. とすることで、改行文字を取り出すことができます。

また、echo の実装には外部の機能を呼び出す !: という conjunction が用いられています 12。各機能には番号が割り当てられていて、1!:2 が出力用の verb です。echo と同等の動作をさせるには、0 0 $ 1!:2&2 とします。

さらに、なんと toupper!: により提供されています131&(3!:12) と書くだけです。

これでようやく全ての名前を取り除くことができました。

(' of beer on the wall.' ,~ 'no more'"_`":@.* , ' bottles' }.~ -@=&1)
(
   (('Go to the store and buy some more, ' , @)99)
   `
   (10 { a. ,~ ('Take one down and pass it around, ' , @))
   @.*
   (]&(0 0 $ 1!:2&2))
   ((1&(3!:12)@{. 0} }: , ', ' , _13&}. , {:)@[.)
)<:"+
i._100

最後に改行と不要なスペースを削除すると、最初にお見せしたプログラムになります。こちらは 227 bytes です。

(' of beer on the wall.',~'no more'"_`":@.*,' bottles'}.~-@=&1)((('Go to the store and buy some more, ',@)99)`(10{a.,~('Take one down and pass it around, ',@))@.*(]&(0 0$1!:2&2))((1&(3!:12)@{.0}}:,', ',_13&}.,{:)@[.))<:"+i._100

おわりに

いかがでしたか。この記事を通して、少しでも J 言語に興味を抱いた方がいらっしゃれば幸いです。

それでは、よき J 言語ライフを。

  1. Tacit Expressions と呼ぶようです。

  2. J Playground はブラウザ上で気軽に試せて良いのですが、2025/03/05 現在、バージョンが J903 なのでエラーになります。

  3. monad (引数が 1 つ) の場合は y、dyad (引数が 2 つ) の場合は xy で引数が渡されます。引数名を指定することはできません。

  4. アンダースコアは数値リテラルのマイナス符号です。例えば _1 2-1 2 とでは動作が異なり、後者はそれぞれに - が適用されて _1 _2 になります。

  5. 演算子の優先順位も無いので、2 * 3 + 42 * (3 + 4) と解釈されます。

  6. 同様に u"1 なら 1 次元配列、u"2 なら 2 次元配列ごとに u が適用されます。

  7. 配列を扱う際の次元数 (rank) などの関係で、厳密には正しくない表現になっています。実際にここでの説明通りの動作をするのは @: および &: です。

  8. null のような特殊な値ではなく、0 × 0 の 2 次元配列です。

  9. 改行を可能にする提案 (Comments on RFC for .. and ...) もありますが、実装されるかは未定です。

  10. ちなみに、modifier train は 1991 年の Version 3.3 で追加 され、2002 年の J 5.01 で削除 された後に、2021 年の J903 で復活 するという、変わった経歴をもつ機能です。

  11. a.i. は変数ではなく、+ などの記号と同じ扱い (primitive) です。変数名は .: を含むことができません。

  12. ライブラリ (DLL) などの利用も、全て !: を介して実行されます。

  13. J による実装も比較的容易で、実際に J903 以前は J で書かれていました。実行速度上の問題からかわかりませんが、J904 で !: に変わったようです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?