はじめに
nibblesで問題を解くの会 第三回です。
一回目、二回目もぜひご覧ください。
今回はmapを扱います。
filterもやろうと思いましたが、思ったより分量が多くなってしまったので、こちらはまた次回で。
この辺りから可読性が終わってきます。
最初はきっと、コードを読んでも何もわからないと思いますが、頑張りましょう。
公式のQuick Refを見ながらだと理解が進みやすいと思います。
ちなみに、nibblesでコメント付けられるの最近まで知らず...今回から活用していきます。
「 # 」以降がコメントになるみたいです。pythonと一緒。
今回はmapの概要を説明した後に問題を解いてみたいと思います。
最終的に、ドキュメントに載っている下記FizzBuzzのコードを解読を目指します!
$?,:^-~%$3"Fizz"^-~%$5"Buzz"
今回もフォーマットはkotatsugameさんのこちらの記事から、丸パクリさせていただきました。(いつもありがとうございます。)
入力形式
解法
コード
コメント
mapでできること
連想配列ではないです。
pythonのmapと基本同じ、内包表記も近いです。
そのため、一部解説の中でpythonの疑似コードを使用しています。(pythonわからない方ごめんなさい)
nibblesにはforがないので、多くの問題でmapを使用することになります。
mapの演算をQuick Refで確認します。
| 演算子 | 引数1 | 引数2 | 
|---|---|---|
| . | list | fn | 
(「 . 」が演算子なのちょっと不思議な感じしませんか?)
map 、第二引数にfn(関数)を取ります。
文字で説明するのも難しいので、実際にコードを見ていきます。
例1)長さ5、要素が全て10の配列を作成してください
pythonの内包表記と上の表のlist, fnを対応させてみました。
map(lambda _: 10, range(5))  # map(fn, list)
[10 for _ in range(5)]  # [fn for _ in list]
nibblesではこちら
.    # map
,5  # list ( ,x で 1 〜 x のリストを作成)
10   # fn
# -> [10, 10, 10, 10, 10]
まだ簡単です。
例2)10〜15のリストを作成してください
先ほどと同様に、まずはpythonを見てみます。
rangeでそのまま実現できますが、今回はこちらで。
map(lambda x: x + 10, range(6))  # map(fn, list)
[x + 10 for x in range(6)]  # [fn for _ in list]
nibblesでこの x をどうやって扱うかがポイントです。
nibblesでは、下記の通り入力を受け取れました。
| Arg | 名前 | 型 | 
|---|---|---|
| $ | fstInt | Integer | 
| @ | fstLine | String | 
| _ | ints | [Integer] | 
| ;$ | sndInt | Integer | 
| ;@ | allLines | [String] | 
| ;_ | allInput | String | 
| ;;$ | intMatrix | [[Integer]] | 
| ;;@ | sndLine | String | 
これに対してmapをしてみます。
.    # map
`,6  # 0 ~ 6 のリストを生成  ( , を `, にすると 0 ~ x - 1のリストが作れます)
実はこのmapで先ほどの表がアップデートされます。
| Arg | New | map前 | 
|---|---|---|
| $ | x(listの中身) | fstInt | 
| @ | fstInt | fstLine | 
| _ | fstLine | ints | 
| ;$ | ints | sndInt | 
| ;@ | sndInt | allLines | 
| ;_ | allLines | allInput | 
| ;;$ | allInput | intMatrix | 
| ;;@ | intMatrix | sndLine | 
| ;;_ | sndLine | - | 
一個ずつ押し出されるイメージです。
そんなわけで、x + 10 は + $ 10 をすれば良いです。
.     # map
`,6   # 0 ~ 10 のリストを生成
+$10  # 足し算
# -> [10, 11, 12, 13, 14, 15]
同じ記号でも意味がどんどん変わってくるのでコードを読むのが難しくなってくるわけです。
mapの中でmapをするとさらに1個分押し出され、押し出され、、逆にmapのスコープを外れるとなんと1個分戻ります。
変数迷子にならないために
変数迷子にならないために、 ct を使ってデバッグすることが重要になってきます。
ct をするとエラー出力で先ほどの変数の表を作ってくれます。
色々見てみましょう。
例1) ctするだけ
入力の受け取り方がそのまま見れます。
ct
nibbles: 
Context:
LetArg inputs
  $ fstInt :: Integer 
  @ fstLine :: String 
  _ ints :: [Integer] 
  ;$ sndInt :: Integer 
  ;@ allLines :: [String] 
  ;_ allInput :: String 
  ;;$ intMatrix :: [[Integer]] 
  ;;@ sndLine :: String 
at line: 1, char: 1
v
ct
^
例2) stringをmap
.   # map
"abcde" # stringもOK
ct  # ctを設置した時点での変数の状況を出力
nibbles: 
Context:
LambdaArg .
  $  :: Char (unused so far, prioritized for implicit args)
LetArg inputs
  @ fstInt :: Integer 
  _ fstLine :: String 
  ;$ ints :: [Integer] 
  ;@ sndInt :: Integer 
  ;_ allLines :: [String] 
  ;;$ allInput :: String 
  ;;@ intMatrix :: [[Integer]] 
  ;;_ sndLine :: String 
at line: 3, char: 1
v
ct
^
例3) たくさんmap
.,10 .,10 .,10 ."abcde" .,10  # たくさんmapしてみる
ct  # この時点での状況を出力
.,10 .,10  # 出力には影響しない部分
nibbles: 
Context:
LambdaArg .
  $  :: Integer (unused so far, prioritized for implicit args)
LambdaArg .
  @  :: Char (unused so far, prioritized for implicit args)
LambdaArg .
  _  :: Integer (unused so far, prioritized for implicit args)
LambdaArg .
  ;$  :: Integer (unused so far, prioritized for implicit args)
LambdaArg .
  ;@  :: Integer (unused so far, prioritized for implicit args)
LetArg inputs
  ;_ fstInt :: Integer 
  ;;$ fstLine :: String 
  ;;@ ints :: [Integer] 
  ;;_ sndInt :: Integer 
  ;;;$ allLines :: [String] 
  ;;;@ allInput :: String 
  ;;;_ intMatrix :: [[Integer]] 
  ;;;;$ sndLine :: String 
at line: 2, char: 1
v
ct
^
問題
Chokudai SpeedRun 002 A - 長方形 α
$ N $
$ A_1 \quad B_1 $
$ A_2 \quad B_2 $
$ \vdots $
$ A_N \quad B_N $
各$ A_i, B_i (i = 1, 2 \cdots N)$に対して、 $ A_i \times B_i $を出力します。
>1;;$`*
実際に問題を解いていきます。
ただ、散々mapの話をしたのにいきなり演算子 . が存在しませんね。一旦気にしないでください。
順番に見ていきます。
まず入力を ;;\$ で受け取ります。
;;$ は数値行をまとめて二次元配列としてアクセスできます。
pythonで次のことをしているイメージ。
[list(map(int, input().split())) for _ in range(XX)]
一行目の$N$は不要ですので、 drop > を使用して読み飛ばします。
# > x で 配列を先頭から x 個分進める
# 例 (pでデバッグできます)
p >3 ,5   # [4,5]
p >6 ,5   # []  要素数より多くてもOK
# 二次元配列に対するdrop
p .,3 ,3  # mapで二次元配列作成 [[1,2,3],[1,2,3],[1,2,3]]
p >1 .,3 ,3  # [[1,2,3],[1,2,3]]
>1;;\$で$ N $を読み飛ばして、 map することで、各$ A_i \quad B_i $に $ でアクセスできるようになります。
.>1;;$ ct
# nibbles: 
# Context:
# LambdaArg .
#   $  :: [Integer] (unused so far, prioritized for implicit args)
# 
# LetArg inputs
#   @ fstInt :: Integer 
#   _ fstLine :: String 
#   ;$ ints :: [Integer] 
#   ;@ sndInt :: Integer 
#   ;_ allLines :: [String] 
#   ;;$ allInput :: String 
#   ;;@ intMatrix :: [[Integer]] 
#   ;;_ sndLine :: String 
# 
# 
# at line: 1, char: 8
#        v
# .>1;;$ ct
# ^
# 入力例1
.>1;;$ p$
# [3,4]
# [1000000000,1]
# [111111111,111111111]
ctで型を確認すると、しっかりと$がIntegerの配列になっていますね。
Integer配列に対して product `* をすると総積が求められるので、
.>1;;$ `*$
.>1;;$`*$  # 不要なスペース削除
.>1;;$`*   # $ 省略
さらに、こちらのImplicit Opsの欄を見ると、下記の記述があります。
If you don't use the accumulator (@) then it will instead assume you wanted to do a map.
簡単に言うと、引数が $ だけのとき、map演算が省略できる場合があるよってことです。
今回はしっかり省略できるので、最終形は下記となります。
>1;;$`*
ABC307 A - Weekly Records
$ N $
$ A_1 \quad A_2 \quad \ldots \quad A_{7N} $
$ A $を7要素ずつ区切って、それぞれ $ sum $ を出力します。
`/7=2;;$+
chunk of を使用して、2行目を7個ずつに分割します。
p `/7 =2;;$  # `/x で配列を x個ずつの要素に分解
# 入力例1
# [[1000,2000,3000,4000,5000,6000,7000],
#  [2000,3000,4000,5000,6000,7000,8000]]
あとは map を使ってそれぞれの要素を sum してあげればOKです。
.`/7=2;;$+$  # map して sum
`/7=2;;$+    # 先頭の map と最後の $ を省略して完成
FizzBuzz
※入力なし
1 ~ 100 の各要素に対して、3の倍数のとき Fizz, 5の倍数のとき Buzz, 3の倍数かつ5の倍数のとき FizzBuzz, それ以外のときは数字のまま出力します。
$?,:^-~%$3"Fizz"^-~%$5"Buzz"
最後にFizzBuzzを扱っていきます。
FizzBuzzは非常に有名な問題ですね。詳細はこちらから。
いままでの問題に対して、かなり難しめですので、気になった方だけ挑戦してみてください!
まずは、「3の倍数に対して、Fizzを出力する」処理だけ抜き出してみます。
これだけでもややこしいので、Quick Refを横に並べてご覧ください!
^-~%$3"Fizz"
# さらに処理を分解
^  # replicate 指定回数文字列を繰り返す
   # 例) ^3"ab" → "ababab"
   # 回数が0以下のときは空文字列
-~ %$3  # - 1 %$3 と同義。
        # 3の倍数のときのみ、 ^ の回数を1とする処理
"Fizz"  # 繰り返し対象の文字列
# テスト
p ^-~%$3"Fizz"
# 入力 出力
# 1   ""
# 2   ""
# 3   "Fizz"
-~ %$3 の補足
普通の引き算をしているだけなんですが、 ~ を使用しています。
~ を使用すると、Quick RefのAutos列の値が使用されます。
- (subtract) のAutosを確認すると 「$1 \quad 1$」 とあります。
第一引数、第二引数それぞれで ~ を使うと 1 として処理されますよ、という意味です。
「 -1\$ 」のように書くと、「-1」というかたまりで扱われてしまうので、- と1の間にスペースが必要なんですね。
~を使用することで「 -~$ 」と短縮できるわけです。
Buzzでも「 ^-~%$5"Buzz" 」と、同様の処理が行われています。
その後、append を使用して、FizzBuzzの結果を連結しています。
:  # append  文字列連結
^-~%$3"Fizz"
^-~%$5"Buzz"
# append テスト
p :"" ""         # ""
p :"Fizz" ""     # "Fizz"
p :"Fizz" "Buzz" # "FizzBuzz"
ここまでの「 :^-~%\$3"Fizz"^-~%\$5"Buzz" 」で
3の倍数のとき Fizz
5の倍数のとき Buzz
3の倍数かつ5の倍数のとき FizzBuzz
の実装ができています。適当な入力で確かめてみてください。
続いて、FizzBuzz以外の数字出力処理を作成していきます。
if/else (lazy list) ?, を使用してFizzBuzzの結果が空文字かどうか、場合分けをしていきます。
?,  # if/else (lazy list)
:^-~%$3"Fizz"^-~%$5"Buzz"
演算結果は
空文字列でない -> true, 空文字列 -> false
となります。
こちらの演算は
「 ?, 条件 trueの出力 falseの出力 」
の形で記載していきます。
p ?,""     "true" "false"  # "false"
p ?,"1"    "true" "false"  # "true"
p ?,""     "true" 10       # "10"
p ?,"Fizz" $      10       # "Fizz"
一番下の処理、実は if/else では、「 条件 」の値が \$ に入ります!
map のように変数の移動が起こるわけですね。
スコープは 「 trueの出力 」だけです。「 falseの出力 」までくると、参照できません。
nibbles: 
Context:
LambdaArg ?,
  $  :: String 
LetArg inputs
  @ fstInt :: Integer 
  _ fstLine :: String 
  ;$ ints :: [Integer] 
  ;@ sndInt :: Integer 
  ;_ allLines :: [String] 
  ;;$ allInput :: String 
  ;;@ intMatrix :: [[Integer]] 
  ;;_ sndLine :: String 
at line: 1, char: 13
            v
p ?, "Fizz" ct
            ^
「 ?,:^-~%\$3"Fizz"^-~%\$5"Buzz" 」の解読に戻ります。
実はこれ、$$が最後に省略されています。
省略分を表示しながら、整理するとこんな感じになります。
?,  # if/else
:   # append
^-~%$3"Fizz"  # Fizz処理
^-~%$5"Buzz"  # Buzz処理
$  # true時の処理  -> FizzBuzz 文字列を表示($:条件部分の値)
$  # false時の処理 -> 数字を表示($:元々の数字)
これで、入力に対して、FizzBuzz出力を行ってくれる部分が作成できました!
p ?,:^-~%$3"Fizz"^-~%$5"Buzz"
# 入力 出力
# 1   "1"
# 2   "2"
# 3   "Fizz"
# 5   "Buzz"
# 15  "FizzBuzz"
# 100 "Buzz"
最後に、1 ~ 100までの数字をmapで処理していけばOKです!
.,100  # 1 ~ 100 のリストをmap
?,:^-~%$3"Fizz"^-~%$5"Buzz"
.,100?,:^-~%$3"Fizz"^-~%$5"Buzz"  # 整理
$?,:^-~%$3"Fizz"^-~%$5"Buzz"      # 公式のコード
なんか先頭だけ違いますね。最後に「 .,100 」と「 $ 」について見ていきます。
こちらのImplicit Opsの欄を再度見ていきましょう。
These rules also apply if the first value is an integer, except that it does a "range from 1 to n" to generate a list first.
数字を先頭に書いたとき、range として扱うよって言ってます(例外アリ)。
そんなわけで、map に加えて range まで省略できるので「 .,100 」が 「 100 」となります!
最後に、「 $ 」と書ける理由についてです。
Quick RefのInputs:欄を見てみます。
defaultという列がありますね。
対象の入力がない場合、入力の値を使用する代わりにdefaultの値が使用されるんですね。
「 $ 」のdefaultをみると、ちょうど100になっております!
そんなわけで、「 100 」が「 \$ 」とできて、下記コードが完成します!
$  # 1 ~ 100 のリストをmap
?,:^-~%$3"Fizz"^-~%$5"Buzz"
$?,:^-~%$3"Fizz"^-~%$5"Buzz"  # 整理
おわりに
急に難易度が爆上がりしてしまいましたが、いかがでしたでしょうか。
記事のことでもなんでも、わからない箇所があれば気軽にご質問ください。
X宛でも構いません、可能な範囲で対応させていただきます!