コード進行をコーディング with Prolog

この記事はFUN Advent Calendar 2018の8日目の記事です。
7日はnatmarkさんの入門Android Neural Networks APIでした。

Prologって

皆さんPrologはご存知でしょうか。 論理型言語と呼ばれる、ルールを決めていくことでプログラムを組み立てる言語です。 C言語と同年代らしい。古文じゃん

未来大知能コース必修のAIプログラミングⅡで習います。私もその際が初見です。 パズルのように普段使ってない部分が刺激されるので、気分転換におすすめ?

Hello Prolog

translate(世界, world).
translate(ワールド, world).
hello(world).

hello(X):-
  translate(X, world).

これをhello.plと保存して”swipl -f hello.pl”と打ち込むとインタプリタが起動します。 このインタプリタに質問をすると、記述したルールに基づいて解を返します。

?- hello(world).
true .

?- hello(X).
X = world ;
X = 世界 ;
X = ワールド .

こんな感じで穴埋め問題のように、欲しいとこに大文字を入れると解を出してくれるっていう言語です(ざっくり)

コード進行をコーディングする

さて、Prologは性質上、定義や規則を表すのに適しています。 コード進行はカデンツ、ダイアトニック、代理コードなどの複数の規則で成り立っているので、勉強用にはちょうど良さそうだと、今回のネタに名が挙がりました。

できたやつ

こちら。 リポジトリはこっち

基音とオプションを選択して送信すると代理コードになりうるコードネームを列挙します。 webアプリは今のところCメジャースケールの代理コードしか実装してないです。

あとこれサーバーも全部Prologで書いてます。気が向いたらFUN-AdventarのPart2の方で枠取って解説するかも

解説

音楽理論のことは説明する余裕がないので雰囲気を感じ取ってください

コードの構成音の表現

scale_list([c, cs, d, ds, e, f, fs, g, gs, a, as, b]).

chord([S1, S2, S3], [S1, '']):-
    Dif12 = 4,
    Dif13 = 7,
    scale_dif(S1, S2, Dif12),
    scale_dif(S1, S3, Dif13).
chord([S1, S2, S3], [S1, 'm']):-
    Dif12 = 3,
    Dif13 = 7,
    scale_dif(S1, S2, Dif12),
    scale_dif(S1, S3, Dif13).

scale_dif(X, Y, Dif):-
    scale_list(Scale),
    append(Scale, Scale, ScaleList),
    count_dif(X, Y, ScaleList, Dif).

count_dif(Y, [Y | _], 0).
count_dif(Y, [_ | L], Num) :- count_dif(Y, L, Num2), Num is Num2 + 1.
count_dif(X, Y, [X | L], Num):- count_dif(Y, L, Num2), Num is Num2 + 1, !.
count_dif(X, Y, [_ | L], Num) :- count_dif(X, Y, L, Num), !.

まずscale_list/1はc, cs, …, bs, aまでのスケール(音符表)を取得する述語です。

chord/2は構成音のリストとコードネームの対応表です。

  • Difは規定された基音からの差。例えばCメジャーなら4番目のEと7番目のG
  • scale_dif/3で基音と2,3番目の音の差を調べ、Difと照らし合わせる

scale_dif/3は音同士のリスト上の距離を測る述語です。

  • ScaleListを2つ繋げているのは、オクターブを跨いだ音に対応するため
  • 実際に距離を測るのはcount_dif/4の仕事

count_dif/3,4はscale_difの実際の処理です。X

  • リスト再帰定義というのを使っています。書けると超気持ちイイ
    1. 最初のcount_diff/3は再帰の停止条件といいます。先頭にXが来た後にYが来た時にNumが0なら最初のNumが解って感じです
    2. 2行目はXが来た後の再帰です。Numを数えながら、Y以外の先頭を削除して次に回します
    3. 3行目は先頭にXが来た時の処理です。引数4から引数3へシフトし、Yを探し始めます 4, 先頭がXでなかった場合は削除して後続のリストで再帰します
  • Prologは同じ述語を上から順番に判定していくので、実際の処理は4から1を目指す感じになります

コード内に!マークが時々出ていることにお気づきでしょうか。カットって言いますが、Prologの気持ちになり切れてないので解説できません。ごめんなさいorz

代理コードの推論

diatonic([Sound, _Option]):- scale_list(ScaleList), nth0(0, ScaleList, Sound).
diatonic([Sound, _Option]):- scale_list(ScaleList), nth0(2, ScaleList, Sound).
diatonic([Sound, _Option]):- scale_list(ScaleList), nth0(4, ScaleList, Sound).
diatonic([Sound, _Option]):- scale_list(ScaleList), nth0(5, ScaleList, Sound).
diatonic([Sound, _Option]):- scale_list(ScaleList), nth0(7, ScaleList, Sound).
diatonic([Sound, _Option]):- scale_list(ScaleList), nth0(9, ScaleList, Sound).
diatonic([Sound, _Option]):- scale_list(ScaleList), nth0(11, ScaleList, Sound).

agency(Chord, Agent):-
    diatonic_scale(Agent),
    chord(SoundC, Chord),
    chord(SoundA, Agent),
    common(SoundC, SoundA),
    Chord \== Agent.

common([X, X1|_], [X, X1|_]):- !.
common([_|X], Y):- common(X, Y), !.
common(X, [_|Y]):- common(X, Y), !.

次代理コード行きます。

diatonic_scale/1は音がスケール内の音か判断する述語です。nth0という組み込み述語で基音からの位置を調べています。

  • このへんをいじってスケール変更に対応したい(未実装)

agency/2は第一引数にコードネームを渡すと代理コードを列挙する述語です。

  • 渡されたコードと提案コードの構成音を取ってきたら、common/2で共通音の数を調べます。

common/2は2つのリストの共通部が2つ以上になるか判定する述語です。これもリスト再帰です。

  1. 最初のcommon/2は、リストの先頭が共通部2つになった際にTrueとなります。これを目指して再帰を回します。
  2. 2行目のcommon/2は1つめのリストの先頭を取り除いてから再評価します。
  3. 3行目も同様に、2つめのリストの先頭を取り除いてから再評価します。
  4. 再帰回しまくってリストが空になったらFalseが返ります。

まとめ

この記事でPrologの魅力が伝わったとは思えないけど、AIの分野でも注目受けてるらしいので気にしておいていい言語だと思います。 とりあえずPrologってツイートしてみようぜ!

次回はph_alkalionさんです。