lambda を隠した書き方 (35) - 第25回シェル芸勉強会 Q2 -- ひらけ!ポンキッキ

  • 2
    いいね
  • 0
    コメント

Schemelambda を隠した書き方で、シェル芸勉強会の問題を解いてみることにした。今回は第25回のQ2。

問題

原文は上田隆一さんの記事にありまふ。

次のような出力を作ってください。
(
"ひらけ!ポンキッキ"
"らけ!ポンキッキひ"
"け!ポンキッキひら"
"!ポンキッキひらけ"
"ポンキッキひらけ!"
"ンキッキひらけ!ポ"
"キッキひらけ!ポン"
"ッキひらけ!ポンキ"
"キひらけ!ポンキッ"
)

作戦

ざっくりこんなふうにしよぉと思う。

  1. 文字列 "ひらけ!ポンキッキ" を文字のリスト (#\ひ #\ら #\け #\! #\ポ #\ン #\キ #\ッ #\キ) にする。
  2. n0リストの長さ - 1 のそれぞれについて以下の処理で得られた値をリストにする。
    1. (drop 文字のリスト n)(take 文字のリスト n) を求める。
    2. drop で求めたリストと take で求めたリストを連結する。
    3. 連結したもの(文字のリスト)を文字列に変換し戻す。

コード

文字のリストに変換

「これは簡単っ!」と思ったら、Guile 特有のワナがっ!・ω・

(echo "ひらけ!ポンキッキ"
  string->list
)
 (#\343 #\201 #\262 #\343 #\202 #\211 #\343 #\201 #\221 #\343 #\203 #\263 #\343 #\202 #\255 #\343 #\203 #\203 #\343 #\202 #\255 #\357 #\274 #\201)

そぉ、UTF-8 表現の char 列が返ってきただけっ。つまり日本語の1文字(1コードポイント)は2個以上の char データに対応してるんだ。

しょおがないから、UTF-8 で1コードポイントを表すバイト列単位で部分リストにすることにしたんだ。

UTF-8 で多バイトのコードポイントを表現するバイト列はこんな感じ…。(矢野啓介さんが書いた技術評論社の「文字コード技術入門」から「表4.2 UTF-8における符号位置とバイト列の対応」を引用して少し変えた。)

コードポイント(16進数) UTF-8 のバイト列(2進数)
000000000000007F 0xxxxxxx
00000080000007FF 110xxxxx 10xxxxxx
000008000000FFFF 1110xxxx 10xxxxxx 10xxxxxx
000100000010FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

とゆうわけで、これを使ってコードポイントごとに部分リストにする処理を追加したコードはこうなった。

(echo "ひらけ!ポンキッキ"
  string->list
  (let
    ((byteLength
      (!:!
       '(
        ((#b11000000 #b11011111) 2)
        ((#b11100000 #b11101111) 3)
        ((#b11110000 #b11110111) 4)
        )
        1
        (lambda (c <>) (<= (car <>) (char->integer c) (cadr <>)))
      )
    ))
    (*~ unfold
      null?
      ($$ (-<$ id ($$ car byteLength)) ($->& take))
      ($$ (-<$ id ($$ car byteLength)) ($->& drop))
    )
  )
)

 ((#\343 #\201 #\262) (#\343 #\202 #\211) (#\343 #\201 #\221) (#\357 #\274 #\201) (#\343 #\203 #\235) (#\343 #\203 #\263) (#\343 #\202 #\255) (#\343 #\203 #\203) (#\343 #\202 #\255))

えっと、!:! はだいぶ前に作った List<Map.Entry>_toFunction 関数の別名。

…で、こぉなると部分リストをリストのままにして置く必要もないし、そもそも何が何だか分かんない。だから部分リストをそれぞれ(一文字の)文字列に変換しもどす。

(echo "ひらけ!ポンキッキ"
  string->list
  (let
    ((byteLength
      (!:!
       '(
        ((#b11000000 #b11011111) 2)
        ((#b11100000 #b11101111) 3)
        ((#b11110000 #b11110111) 4)
        )
        1
        (lambda (c <>) (<= (car <>) (char->integer c) (cadr <>)))
      )
    ))
    (*~ unfold
      null?
      ($$ (-<$ id ($$ car byteLength)) ($->& take))
      ($$ (-<$ id ($$ car byteLength)) ($->& drop))
    )
  )
  (each list->string)
)

 ("ひ" "ら" "け" "!" "ポ" "ン" "キ" "ッ" "キ")

うんっ、いい感じ。^_^

n0リストの長さ - 1 のそれぞれ…

これは簡単で、length 関数でリストの長さを求めてから iota 関数を使えばいぃ。

(echo "ひらけ!ポンキッキ"
  ~これまでのコード~
  ($$ length iota)
)

 (0 1 2 3 4 5 6 7 8)

だけど、これだともとのリストがなくなっちゃうんで、もとのリストもスルーさせとく。

(echo "ひらけ!ポンキッキ"
  ~これまでのコード~
  (-<$
    id
    ($$ length iota)
  )
)

 (("ひ" "ら" "け" "!" "ポ" "ン" "キ" "ッ" "キ") (0 1 2 3 4 5 6 7 8))

そして、数字をもとのリストにばらまく。これには distribute-former-to-latter-fore 関数を使う。

(echo "ひらけ!ポンキッキ"
  ~これまでのコード~
  (-<$
    id
    ($$ length iota)
  )
  ($->& distribute-former-to-latter-fore)
)

 (
  (("ひ" "ら" "け" "!" "ポ" "ン" "キ" "ッ" "キ") 0)
  (("ひ" "ら" "け" "!" "ポ" "ン" "キ" "ッ" "キ") 1)
  (("ひ" "ら" "け" "!" "ポ" "ン" "キ" "ッ" "キ") 2)
  (("ひ" "ら" "け" "!" "ポ" "ン" "キ" "ッ" "キ") 3)
  (("ひ" "ら" "け" "!" "ポ" "ン" "キ" "ッ" "キ") 4)
  (("ひ" "ら" "け" "!" "ポ" "ン" "キ" "ッ" "キ") 5)
  (("ひ" "ら" "け" "!" "ポ" "ン" "キ" "ッ" "キ") 6)
  (("ひ" "ら" "け" "!" "ポ" "ン" "キ" "ッ" "キ") 7)
  (("ひ" "ら" "け" "!" "ポ" "ン" "キ" "ッ" "キ") 8)
  )

(drop 文字のリスト n)(take 文字のリスト n) を求める。

これも簡単で、

(-<$
  ($->& drop)
  ($->& take)
)

を、上から流れてくるリストのそれぞれについてやればいいだけ。

(echo "ひらけ!ポンキッキ"
  ~これまでのコード~
  (each
    (-<$
      ($->& drop)
      ($->& take)
    )
  )
)

 (
  (("ひ" "ら" "け" "!" "ポ" "ン" "キ" "ッ" "キ") ())
  (("ら" "け" "!" "ポ" "ン" "キ" "ッ" "キ") ("ひ"))
  (("け" "!" "ポ" "ン" "キ" "ッ" "キ") ("ひ" "ら"))
  (("!" "ポ" "ン" "キ" "ッ" "キ") ("ひ" "ら" "け"))
  (("ポ" "ン" "キ" "ッ" "キ") ("ひ" "ら" "け" "!"))
  (("ン" "キ" "ッ" "キ") ("ひ" "ら" "け" "!" "ポ"))
  (("キ" "ッ" "キ") ("ひ" "ら" "け" "!" "ポ" "ン"))
  (("ッ" "キ") ("ひ" "ら" "け" "!" "ポ" "ン" "キ"))
  (("キ") ("ひ" "ら" "け" "!" "ポ" "ン" "キ" "ッ"))
  )

いい感じだねっ。(・∀・)

drop で求めたリストと take で求めたリストを連結する。

連結には concatenate 関数を使えばいぃ。

(echo "ひらけ!ポンキッキ"
  ~これまでのコード~
  (each concatenate)
)

 (
  ("ひ" "ら" "け" "!" "ポ" "ン" "キ" "ッ" "キ")
  ("ら" "け" "!" "ポ" "ン" "キ" "ッ" "キ" "ひ")
  ("け" "!" "ポ" "ン" "キ" "ッ" "キ" "ひ" "ら")
  ("!" "ポ" "ン" "キ" "ッ" "キ" "ひ" "ら" "け")
  ("ポ" "ン" "キ" "ッ" "キ" "ひ" "ら" "け" "!")
  ("ン" "キ" "ッ" "キ" "ひ" "ら" "け" "!" "ポ")
  ("キ" "ッ" "キ" "ひ" "ら" "け" "!" "ポ" "ン")
  ("ッ" "キ" "ひ" "ら" "け" "!" "ポ" "ン" "キ")
  ("キ" "ひ" "ら" "け" "!" "ポ" "ン" "キ" "ッ")
  )

連結したもの(文字のリスト)を文字列に変換し戻す。

(echo "ひらけ!ポンキッキ"
  ~これまでのコード~
  (each ($->& string-append))
)

 (
  "ひらけ!ポンキッキ"
  "らけ!ポンキッキひ"
  "け!ポンキッキひら"
  "!ポンキッキひらけ"
  "ポンキッキひらけ!"
  "ンキッキひらけ!ポ"
  "キッキひらけ!ポン"
  "ッキひらけ!ポンキ"
  "キひらけ!ポンキッ"
  )

できたっ。(*´ω`*)

全部のコード

(echo "ひらけ!ポンキッキ"
  string->list
  (let
    ((byteLength
      (!:!
       '(
        ((#b11000000 #b11011111) 2)
        ((#b11100000 #b11101111) 3)
        ((#b11110000 #b11110111) 4)
        )
        1
        (lambda (c <>) (<= (car <>) (char->integer c) (cadr <>)))
      )
    ))
    (*~ unfold
      null?
      ($$ (-<$ id ($$ car byteLength)) ($->& take))
      ($$ (-<$ id ($$ car byteLength)) ($->& drop))
    )
  )
  (each list->string)

  (-<$
    id
    ($$ length iota)
  )
  ($->& distribute-former-to-latter-fore)
  (each
    (-<$
      ($->& drop)
      ($->& take)
    )
  )
  (each concatenate)
  (each ($->& string-append))
)

おまけ: 「𩸽ほっけ

UTF-8 の処理が本当にうまく出来てるのか…。今回はたまたま UTF-8 のバイト列が3バイトになる文字だけだったけど、4バイトになる文字だったり、1バイトになやる文字が混在していたら?試してみた。文字「𩸽ほっけ」は4バイトになる文字だからこれも混ぜて。それから1バイトになる文字として半角の「Ra」も混ぜた。

(echo "ひRaけ𩸽ポンキッキ"
  ~作ったコード~
)

 (
  "ひRaけ𩸽ポンキッキ"
  "Raけ𩸽ポンキッキひ"
  "aけ𩸽ポンキッキひR"
  "け𩸽ポンキッキひRa"
  "𩸽ポンキッキひRaけ"
  "ポンキッキひRaけ𩸽"
  "ンキッキひRaけ𩸽ポ"
  "キッキひRaけ𩸽ポン"
  "ッキひRaけ𩸽ポンキ"
  "キひRaけ𩸽ポンキッ"
  )

グッジョブ \(^o^)/

参考

関数 別名 機能 定義
concatenate リストの入れ子を一階層だけ平坦化したリストを返す。 SRFI-1
distribute-former-to-latter-fore 第2引数として与えられたリストの要素のそれぞれと、第1引数として与えられたオブジェクトを組み合わせた(リストの)リストを返す。 →「lambda を隠した書き方 (6) - 直積、組み合わせ
drop 第1引数で与えられたリストの要素を前の方から第2引数で指定された個数だけ取り除いて残った部分リストを(生成して)返す。 SRFI-1
each @$ “並列化関数”
与えられた関数を map に部分適用した関数を返す。
(Function_curryFore map)
echo 第2引数以降の引数として与えられた関数を合成した関数を作り、その合成関数に対して第1引数で与えられ値を適用して、その評価値を返す。 → 「lambda を隠した書き方 (8) - 個数を数える
Function_bindFore *~ “前側束縛関数”
引数列の前側を束縛した関数を返す
→「lambda を隠した書き方
Function_compose $$ “合成化関数”
与えられた関数を合成した関数を返す。
→「lambda を隠した書き方
Function_curryFore ?~ “前側カリー化関数”
何かを受け取ると関数の引数列の前側に束縛してくれる…そぉいう関数を返す。
→「lambda を隠した書き方 (4) - カリー化的な?
Functions_funcaller -<$ “分岐関数”
引数を、関数列に対して分配的に(funcall式に)適用してくれる関数を返す。
→「lambda を隠した書き方 (4) - カリー化的な?
Function_toApplyStyle $->&、
apply@
引数の受け取り方を apply 式(ひとつのリストとしてまとめて受け取る方式)に変更した関数を返す。 (Function_curryFore apply)
id “恒等写像”
与えられた値をそのまま返す。
→「lambda を隠した書き方 (2)
List<Map.Entry>_toFunction !:! 条件値とその時の値のリスト、そして既定値となる関数を受け取って、場合分け関数を作って返す。 →「lambda を隠した書き方 (12) - 場合分け、その3
take 第1引数で与えられたリストの要素を前の方から第2引数で指定された個数だけ切り出した部分リストを(生成して)返す。 SRFI-1
unfold 第4引数で与えられた“種”に、第3引数で与えられた関数をどんどん適用して“成長”させる。成長する都度、第2引数で与えられた関数を使って“果実”を産み落とす。第1引数を満たすまでに成長したら成長を止めて、成長の過程で産み落とされた果実を順に並べたリストを返す。 SRFI-1


≪前: lambda を隠した書き方 (34) - 二分岐の拡張
次≫: lambda を隠した書き方 (36) - 第25回シェル芸勉強会 Q6 -- UTF-16LE