0
0

More than 1 year has passed since last update.

AI 術士に負けるな!今なら間に合うアブラカダブラ

Last updated at Posted at 2023-08-23

AI 術士なる耳慣れない言葉が世の耳目を引く今日この頃、いかがお過ごしでしょうか?

日夜、「すてーぶるでぃふゅーじょん」なる所で苛烈な呪文開発競争が行われているそうです。これは負けていられませんね。

ですので、西洋呪術の王道アブラカダブラで迎え撃ちます。

我々は本格派ですから、さらに呪文の回文化を行い万全を期します。

ついでにコーディングで得られた知見とサンプルコードを共有します。

前提条件

  • 回文プログラム
    • 文字列を引数に取り、返り値に回文を返す関数
    • 回文は引数に引数の逆順を結合する「パワー系回文」方式
    • 最後の 1 文字は繰り返さない

これ単体ではあまり面白くないが、次のプログラムのパーツとなります。

  • アブラカダブラ回文プログラム
    • 回文プログラムを再帰的に適用することでアブラカダブラ風の呪文を作る関数

具体的には次のような答えを返すプログラムを作ります。

goal.txt
想定される答え:
ABCDEDCBA
ABCDCBA
ABCBA
ABA
A

なお、実在するアブラカダブラの呪文が書かれた護符は次のように中央揃えされています(さらに言うと回文である必要はない)。

ABCDEDCBA
 ABCDCBA
  ABCBA
   ABA
    A

知見とサンプルコード

クロージャを使うとグローバル変数を使わなくてもすむ

kaibun.js
function kaibun(input) {
  const res = [];
  let word = input;

  return {
    main() {
      if (word.length > 1) {
        const palindrome =
          word +
          [...word]
            .slice(0, word.length - 1)
            .reverse()
            .join("");
        console.log(palindrome);
        word = word.slice(0, word.length - 1);
        res.push(palindrome);
      } else if (word.length === 1) {
        console.log(word);
        res.push(word);
        word = "";
      }
    },
    palindrome() {
      const palindrome = res.map((e, num) => " ".repeat(num) + e);
      console.log(palindrome.join("\n"));
      return palindrome.join("\n")
    },
    value() {
      console.log(res, word);
    },
  };
}

クロージャとは、関数内で定義された関数が、その外側の関数の変数にアクセスできるようにする仕組みです。ここでは、呪文の記録を保持するために使われています。

これでアブラカダブラの呪文を生成できるようになりましたが、毎回文字数の回数 kaibun.main() を呼び出すのは面倒ですね。

次は、一発で呪文を生成できる関数を作ります。

繰り返しは再帰を使うとスッキリした見た目になる

spellMaker.js
const abracadabra = (input) => {
     if (input.length == 0) {
         return ""
     }
     // 呪文は引数の最後の文字を軸に回文を形成する。最後の文字は増えない。
     const line = input + [...input].reverse().join("").substr(1) + "\n";
     const next_line = abracadabra(input.substr(1));
     return line + next_line
 }

再帰とは、プログラム内で自身を呼び出すことで同じ操作を繰り返す手法です。

コードがとても短くなりました。一発で呪文が帰ってくるようになったので便利です。

でも、まだ手を加える余地があります。例えば次の行とかコンテキストが脳内から蒸発した 1 週間後とかに読みたくないな〜って感じです。

 const line = input + [...input].reverse().join("").substr(1) + "\n";

読むべきところを分離し、変数名を気をつけると読みやすくなる

GenerateSymmetricPattern.js
const GenerateSymmetricPattern = (input) => {
  const splitAtAxis = (input) => {
    function _split(input) {
      const [axis, outer] = [input.pop(), input.slice()];
      const reversed_outer = input.reverse();
      return [outer.join(""), axis, reversed_outer.join("")];
    }
    return _split([...input]);
  };
  const [outer, axis, reversed_outer] = splitAtAxis(input);

  function main(input) {
    if (input.length === 0) return "";

    const pattern = `${outer}${axis}${reversed_outer}\n`;
    const nextPattern = GenerateSymmetricPattern(input.substr(1));
    return `${pattern}${nextPattern}`;
  }
  return main(input);
};

全体的に長くなりましたが、すべてを読む必要はありません。内容を理解するために読む必要があるのは main() だけになっています。

次に示すコードの主要部分を読んでみてください、先程のコードと違い変数が意味がある言葉になっています。

const pattern = `${outer}${axis}${reversed_outer}\n`;
const nextPattern = GenerateSymmetricPattern(input.substr(1));
return `${pattern}${nextPattern}`;

Python での実装

勘のいい皆様のことですから、いろいろ書いてはいるが Python で型ヒントとスライス表記を使った方が簡単な問題じゃんとお考えでしょう。

abracadabra.py
def abracadabra(string:str) -> str:
    """Python のスライスの力でスッキリと書き換え"""
    if len(string) == 1:
        return string
    def power_kaibun(string):
        return string + string[-2::-1] + "\n"
    return power_kaibun(string) + abracadabra(string[:-1])

power_kaibun() なんてローマ字で命名しちゃいけないとお考えの皆様のためにラムダ式を使ったコードも考えてみましょう。

lambda_abracadabra.py
def abracadabra(string:str) -> str:
    """ラムダ式で名前を考える必要をなくした"""
    if len(string) == 1:return string
    return (lambda x: x + x[-2::-1] + "\n")(string) + abracadabra(string[:-1:])

非常にスッキリしました。この記事ではコードの簡潔さと可読性を追求してきましたが、これで完成と見ていいでしょう。

終わりに、あるいは問題の抽象化

アブラカダブラ回文の問題を抽象化すると次のような要素に分解できます。

  1. 要素数が一定数ずつ減る配列を対象にする
  2. 同じ処理を繰り返す
  3. 型 A の配列を引数にとり、 A を返す

最期に、同様の要素からなる「整数の配列の総積」を求める問題を解き、一見すると別の問題が実は同じ問題であり、そっくりなコードで解けることを示します。

JavaScript での実装

multi.js
// 配列の数字の積を返す関数
// - 数字以外が入った配列はそのまま返す
const multi = (int_Array) => {
  if (!Array.isArray(int_Array)) {
    return int_Array
  } else if (int_Array.some((e) => !(Number.isFinite(e)))) {
    return int_Array
  }

  if (int_Array.length === 1) {
    return int_Array[0];
  }

  const tail = int_Array[int_Array.length - 1];
  const not_tail = int_Array.slice(0, int_Array.length - 1);

  return tail * multi(not_tail);
};

const PASS_CASE = () => multi([1,2,3,4,5]) == 120 ? console.log("PASS!") : console.error("ERROR!");
const NaN_CASE = () => multi([1,2,NaN,4,5]).toString() == [1,2,NaN,4,5].toString() ? console.log("PASS!") : console.error("ERROR!");

PASS_CASE();
NaN_CASE();

Python での実装

multi.py
""" Python の型ヒントとスライスを使ってスッキリと multi.js を書き換える  """

def multi(int_array: list[int]) -> list[int] | int:
    """ 配列の数字の積を返す関数 """
    if len(int_array) == 1:
        return int_array[0]

    tail:int = int_array[-1]
    # int_array[0:-2] でないことに注意!
    not_tail:list[int] = int_array[0:-1]

    return tail * multi(not_tail)

print(multi([1,2,3,4,5]))# 120
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