プログラミングの備忘録

プログラムをつくる過程を残すもの

Project Euler を通して Haskell を学ぼう

こんにちは。
今回は Project Euler の第1問から第10問まで Haskell で解いてみようと思います。


Project Euler は、時間制限なしの競技プログラミングのようなもので、過去に紹介しています。

taq.hatenadiary.jp


そして、Haskell も最近始めました。

taq.hatenadiary.jp

実は割と気に入っていて、何かしたいなと思っていたところ Project Euler の存在を思い出しました。

Python で解いていって記事を書いているのですが、Haskell でも練習がてら解いてみます。
(ここ1年くらい更新が止まっていますが…)

taq.hatenadiary.jp


以下、ネタバレになる (かもしれない) ので、嫌だという方は先に解いてみてください。

projecteuler.net


目次


Problem 1 -Multiples of 3 or 5-

10 より小さい 3 または 5 の倍数を並べると 3 5 6 9 となり、これらの和は 23 である。
1000 より小さい 3 または 5 の倍数の和を求めよ。


main = print $ sum [x | x <- [0..999], x `mod` 3 == 0 || x `mod` 5 == 0]

以上です。簡単。

0 から 999 の中から条件を満たす数を選んでリストに入れていき、最後に各要素の和をとって出力すれば完了です。
答えは 233168 と出力され、正解でした。


ちなみに、sum は以下のように定義できます。

sum' [] = 0
sum' (x:xs) = x + sum' xs

引数で受け取ったリストの先頭を足して残りを sum' に、を繰り返していきます。
引数が空のリストになったら 0 を足して終了です。


Problem 2 -Even Fibonacci Numbers-

フィボナッチ数列のある項は、その前2つの数字を足すことで計算される。
1 2 から始めると、初めの10項は 1 2 3 5 8 13 21 34 55 89 となる。
400万を超えない範囲で考えたとき、偶数のフィボナッチ数を全て足すといくらになるか。


fib = 1 : 2 : zipWith (+) fib (tail fib)
main = print $ sum [x | x <- takeWhile (< 4000000) fib, x `mod` 2 == 0]


フィボナッチ数列を出力するプログラムは、Haskell を紹介したときに書きました。
とりあえず zipWith を使ったものを選びました。
問題文から、リストの1, 2番目の要素は 1 2 になるように変更しています。

fib はリストになっているので、400万より小さい要素を取り出し、2 の倍数 (偶数) のものだけを選び、最後に和をとって完了です。
答えは 4613732 と出力され、正解でした。


ちなみに、takeWhile は以下のように定義できます。

takeWhile' c (x:xs)
   | c x = x : takeWhile' c xs
   | otherwise = []

c は条件式を指し、これをリストの先頭の要素に適用した c x が真のときは、その要素をリストにいれて残りの部分を再び takeWhile' に渡します。
偽のときは空のリストを返し、繰り返しが終わります。

また、ここで使っているのは | を行頭において条件を示す書き方で、数式を

$$ f(x) = \begin{cases} x & (x \geq 0) \\ -x & (x < 0) \end{cases} $$

のように書くときと似ています。
(条件と返り値の位置が逆ですが。)


Problem 3 -Largest Prime Factor-

13195 の素因数は 5 7 13 29 である。
600851475143 の素因数の中で最大のものは何か。


factorize 1 = []
factorize n = f : factorize (n `div` f)
  where f = [x | x <- [2..], n `mod` x == 0] !! 0

main = print $ factorize 600851475143


factorize素因数分解をする関数です。

fn の最小の約数 ([x | x <- [2..], n mod x == 0] !! 0) を受け取っています。
2以上の整数 xn を割り、割り切れた場合 xn の約数だとわかるので f に入れる、という感じです。
これは遅延評価の為せる技で、約数が1つでも見つかれば f に渡せるので無駄な計算が行われません。

f が求まったところで、それをリストにいれるとともに n div ffactorize に渡します。
あとは同じことが繰り返され、n div f1 になったところで終了です。

(「``」がはてなブログと干渉してしまったので、moddiv を囲う記号は説明文では省いてあります。)

素因数分解の結果、[71,839,1471,6857] が出力され、正解でした。


Problem 4 -Largest Palindrome Product-

回文数はどちらから読んでも同じである。
2桁の数の積として表せる最大の回文数は 9009 = 91 × 99 である。
3桁の数の積として表せる最大の回文数は何か。


main = print $ maximum [x | x <- [a * b | a <- [100..999], b <- [100..999]], show x == reverse (show x)]


[a * b | a <- [100..999], b <- [100..999]] で総当たりで積を計算し、show x == reverse (show x)回文数かどうか判定します。
回文数になったものだけをリストにいれ、その中から最大のものを出力します。
答えとして 906609 が出力され、正解でした。

ここで、show は数値を文字列に変換する関数、reverse は順序を逆にする関数です。
(ちなみに、文字列を数値に変換するときは read 文字列 :: Int のようにします。)


reversemaximum は以下のように定義できます。

reverse' [] = []
reverse' (x:xs) = reverse' xs ++ [x]
maximum' [] = []
maximum' (x:xs) = max' x (maximum' xs)
  where max' x y
          | x > y = x
          | otherwise = y


Problem 5 -Smallest Multiple-

2520 は 1 から 10 の数で割り切ることができる最小の数である。
1 から 20 の数で割り切ることができる自然数の中で最小のものは何か。


main = print $ foldr lcm 1 [1..20]


Python で解いたときにも書いていますが、1~20 で割り切れるということは $20!$ かというとそうではなく、最小公倍数を考える必要があります。

引数に渡した2つの数の最小公倍数を求める関数 lcm がありますので、それを利用します。

fn [] = 1
fn (x:xs) = lcm x (fn xs)

main = print $ fn [1..20]

と書けば良いのですが、fn と似た機能を持った関数が既にあります。
それが foldr です。

foldr は以下のような定義ですので、flcmv1 に対応していることになります。

foldr' f v [] = v
foldr' f v (x:xs) = f x (foldr' f v xs)


プログラムを実行すると、答えとして 232792560 が出力され、正解でした。


ちなみに、lcm は以下のように定義できます。

gcd' x y
  | y == 0 = x 
  | otherwise = gcd' y (x `mod` y)

lcm' x y = x * y `div` gcd' x y

ここで gcd' は最大公約数を求める関数で、ユークリッドの互除法 (ユークリッドの互除法 - Wikipedia) を使っています。
定義をそのまま書いたような感じですがうまく動きました。
そして、$\mathrm{gcd} (x, y) \times \mathrm{lcm} (x, y) = a \times b$ と表せるので、$\displaystyle \mathrm{lcm} (x, y) = \frac{a \times b}{\mathrm{gcd} (x, y)}$ で最小公倍数が求められます。


また、foldr の親戚に foldr1 という関数があります。
これは空のリストが渡されるとエラーを吐くようになっており、その一歩手前、すなわち配列の最後の要素を v として計算してくれる foldr です。

foldr1' f [v] = v
foldr1' f (x:xs) = f x (foldr1' f xs)

これを使えば、この問題ではわざわざ v を指定しなくても良くなります。

main = print $ foldr1 lcm [1..20]


Problem 6 -Sum Square Difference-

1 から 10 までの自然数の2乗の和は 385、和の2乗は 3025 である。
よって、この2つの差は 3025 - 385 = 2640 となる。
同じことを 1 から 100 までの自然数で行うと答えはどうなるか。


main = print $ (sum [1..100])^2 - sum [x^2 | x <- [1..100]]


単純に計算すれば良いだけです。
実行すると 25164150 と出力され、正解でした。


Problem 7 -10001st Prime-

初めの6つの素数を並べると 2 3 5 7 11 13 となり、6番目の素数は 13 だとわかる。
10001番目の素数は何か。


sieve [] = []
sieve l@(x:xs) = x : sieve [a | a <- l, a `mod` x /= 0]

main = print $ (sieve [2..]) !! 10000


sieve では 2 以上の整数の無限リストを渡すと、その中から素数だけを返してくれます。
いわゆる「エラトステネスのふるい」を使っており、計算自体は Wikipedia (エラトステネスの篩 - Wikipedia) にあるのと同様です。
受け取ったリストの先頭の数の倍数を除いて、その結果をまた sieve に渡す、という感じです。

l@(x:xs) は、受け取ったリストを変数に束縛しています。
(x:xs) はリストの先頭を x に、残りを xs に入れることを意味しており、@ を挟んで何か変数 (ここでは l) をつければその変数にリスト全体を入れることができます。

答えとして 104743 が出力され、正解でした。


Problem 8 -Largest Product in a Series-

問題文にある1000桁の数の中で隣接した4個の数の積の最大値は 9×9×8×9 = 5832 である。
隣接する13個の数の積の最大値はいくつか。


num = "7316717653133062491922511967442657474235534919493496983520312774506326239578318016984801869478851843858615607891129494954595017379583319528532088055111254069874715852386305071569329096329522744304355766896648950445244523161731856403098711121722383113622298934233803081353362766142828064444866452387493035890729629049156044077239071381051585930796086670172427121883998797908792274921901699720888093776657273330010533678812202354218097512545405947522435258490771167055601360483958644670632441572215539753697817977846174064955149290862569321978468622482839722413756570560574902614079729686524145351004748216637048440319989000889524345065854122758866688116427171479924442928230863465674813919123162824586178664583591245665294765456828489128831426076900422421902267105562632111110937054421750694165896040807198403850962455444362981230987879927244284909188845801561660979191338754992005240636899125607176060588611646710940507754100225698315520005593572972571636269561882670428252483600823257530420752963450"
digit = 13

products = [product (separate x) | x <- adjacents num digit]
  where separate l = [read [l !! i] :: Int | i <- [0..(length l)-1]]
        adjacents l n = [extract l i n | i <- [0..(length l)-n]]
        extract l i n = [l !! (i+x) | x <- [0..n-1], i+x < length l]

main = print $ maximum products


なかなか煩雑になってしまいました。

extractl の中の i 番目の要素から n 個分取り出してくる関数です。
adjacentsl の中から n 個取り出したときに得られる数列全てを返す関数です。
separatel から取り出した数列を1文字ずつに区切り、さらに数値に変換します。
products は取り出した数の各桁の積を計算し、その結果を全て入れておくリストです。
最後に products の中から最大値を探し、出力します。

答えとして 23514624000 が出力され、正解でした。


余談ですが、read 文字列 :: Int の文字列の部分は String 型でないといけないようです。
l !! i だけだと一文字に相当する Char 型として扱われてしまうので [] で囲うことでリスト化し、String 型に変換しています。


Problem 9 -Special Pythagorean Triplet-

ピタゴラス数は以下の条件を満たす3つの数の組み合わせである。
$$ a^{2} + b^{2} = c^{2}, ~ a < b < c $$ 例えば、$3^{2} + 4^{2} = 9 + 16 = 25 = 5^{2}$ となる。
$a + b + c = 1000$ となるピタゴラス数が1組存在するが、その積 $abc$ は何か。


triplets l = [[a, b, c] | m <- [1..l], n <- [1..m], let a = m^2-n^2, let b = 2*m*n, let c = m^2+n^2, a + b + c == l]
main = print $ product $ (triplets 1000) !! 0


Wikipedia (ピタゴラス数 - Wikipedia) から、自然数 $m$ と $n$ について、$m > n$、$(a, b, c) = (m^{2}-n^{2}, 2mn, m^{2}+n^{2})$ を満たせば良いとわかるので、それをそのまま書いたような感じです。
また、各辺の和が 1000 になるという条件もあるので、$a + b + c = 1000$ も追加しています。

[375,200,425] が見つかり、積を計算して 31875000 が出力されます。正解でした。


Problem 10 -Summation of Primes-

10以下の素数の和は 2 + 3 + 5 + 7 = 17 である。
200万以下の素数の和はいくらになるか。


primes = 2 : [x | x <- [3,5..], isPrime x]
  where isPrime n = all (\p -> n `mod` p /= 0) (takeWhile (\p -> p*p <= n) primes)

main = print $ sum $ takeWhile (< 2000000) primes


Problem 7 でつくったエラトステネスのふるいが役に立ちます。
のはずでしたが、10001番目の出力でも若干時間がかかっていたこともあってか、

sieve [] = []
sieve l@(x:xs) = x : sieve [a | a <- l, a `mod` x /= 0]

main = print $ sum $ takeWhile (< 2000000) (sieve [2..])

としても一向に答えが出力されませんでした。


というわけで、初めに載せたような解法を考えました。

試し割り法でもエラトステネスのふるいでも、考える数の最大値の平方根まで試せば良いので、判定する数列は takeWhile (\p -> p*p <= n) primes としています。
ここで、\p -> p*p <= n のような式は「ラムダ式」とよばれる関数で、この式は p を受け取って p*p <= n の真偽を返す関数となっています。
(ラムダ式に関しては記事を作成中です。(いつに公開できるようになるかわかりませんが。))

さらに、2 以外の偶数は全て合成数のはずなので、primes には先に 2 を入れておき、素数かどうかの判定 isPrime は奇数 [3,5..] のみを考えています。

isPrime では、takeWhile (\p -> p*p <= n) primes として取ってきたリスト全体に対して、\p -> nmodp /= 0 が真であった場合に True を返します。

4秒程度で 142913828922 と出力され、正解でした。


ちなみに、all は以下のように定義できます。

all' c [] = True
all' c (x:xs)
  | c x = all' c xs
  | otherwise = False


まとめ

以上、Project Euler の問題を Haskell で解いてみました。

書いてみたらコードゴルフでもしているのかというくらい短くなっておもしろいですね。
でもまだまだ勉強が足りず、検索や ChatGPT に頼りまくってました。

冒頭でも書きましたがやっぱり私はこの言語が好きかもしれません。
続編をお楽しみに。


最後まで読んでいただいてありがとうございました。また次回。

プログラミング言語「Chef」を触ってみる

こんにちは。
今回は、プログラミング言語「Chef」を紹介します。


一風変わったプログラミング言語シリーズ (?) の4つ目です。

プログラミング言語「Piet」を使ってみる - プログラミングの備忘録

プログラミング言語「Uiua」を使ってみる - プログラミングの備忘録

プログラミング言語「Shakespeare」を触ってみる - プログラミングの備忘録


目次


「Chef」とは

Chef (DM's Esoteric Programming Languages - Chef) はプログラミング言語のひとつです。
これは全くの偶然なのですが、Chef の開発者と以前紹介した Piet の開発者は同じ方のようです。

名前の通り料理に関係しており、「コードがレシピに見えるように」というコンセプトがあるそうです。
(実際に「Hello World!」を出力するコードをつくり、そのコード (レシピ) 通りに「Hello Worldケーキ」をつくった方 (Baking a Hello World Cake | Products of Mike's Mind) もいます。)


仕様

DM's Esoteric Programming Languages - Chef の内容を要約していきます。

Chef の "レシピ" には以下の要素が登場します。(一部は無くても良いです。)
各要素はここに示した順で、間に1行の空白行を挟んで書いていきます。

レシピタイトル

プログラムが何をするものなのかを記述します。
1行目でピリオドまでがタイトルになります。

コメント

何かあれば書いてください。

材料リスト

プログラム内で使う変数を材料として示し、各材料は量として数値を指定します。
初期値 単位 材料名 の形で羅列していきます。
(初期値や単位は無く材料名だけを宣言しても良いようです。)

単位と、固体・液体の関係
固体: gkgpinch[es] (つまみ)、heaped (山盛りの)、level (一杯の)
液体: mlldash[es] (少量)
未特定: cup[s]teaspoon[s]tablespoon[s]

Ingredients.
101 eggs
108 g lard
1 cup white sugar
119 ml water

出力においては、液体は Unicode で対応する文字として、固体 or 未特定の材料は数値として出力されます。

調理時間

記載は任意です。
単位は hour[s]minute[s] です。
(コードの作成時間でも書いておけば良いでしょうか。)

Cooking time: 30 minutes.

オーブンの温度

記載は任意ですが、焼く工程があるときは予熱をしておく必要があるでしょう。

degrees Celsius は摂氏温度 [℃]、gas mark はイギリスなどで使われるオーブンの温度単位 (Gas mark - Wikipedia) のようです。
(gas mark は書かなくても良いです。)

Pre-heat oven to 200 degrees Celsius (gas mark 6).

手順

調理手順を記載します。
プログラムの本文にあたります。

ボウルと耐熱皿

Chef にはスタックのような役割を持つボウルと耐熱皿が存在します。

ボウルと耐熱皿は材料を入れることができ、パンケーキを積み重ねるように順番に入っていきます。
材料に対応する値が変わったとしてもこれらの容器の中では材料の値は変わりません。

容器が複数あっても序数 (1st, 2nd, 3rd, 4th, ...) で区別可能です。

命令

以下、命令とその意味です。

  • Take 材料名 from refrigerator.: 入力を受け取り、指定した材料に代入する

  • Put 材料名 into mixing bowl.: ボウルに材料を入れる

  • Fold 材料名 into mixing bowl.: ボウルから一番上の値を取り出し、指定した材料に代入する

  • Add 材料名 [to mixing bowl].: ボウルの一番上の値に指定した材料の値を足す

  • Remove 材料名 [from mixing bowl].: ボウルの一番上の値から指定した材料の値を引く

  • Combine 材料名 [into mixing bowl].: ボウルの一番上の値と指定した材料の値をかける

  • Divide 材料名 [into mixing bowl].: 指定した材料の値をボウルの一番上の値で割る

  • Add dry ingredients [to mixing bowl].: 固体の材料の値を全て足し、ボウルに入れる

  • Liquefy 材料名.: 指定した材料を液体にする

  • Liquefy contents of the mixing bowl. ボウルの中の全ての材料を液体にする
    (液体化は Unicode で対応する文字として出力するためなどに使う)

  • Stir [the mixing bowl] for 数値 minutes.: ボウルの一番上の値を数値の分だけ "ロール" する
    (ロール: 一番上の値を 数値 個下に動かし、それより上の値を上に動かす)

  • Stir 材料名 into the mixing bowl.: ボウルの中の指定した材料を、その値分だけロールする

  • Mix [the mixing bowl] well.: ボウルの中の材料をランダムに並べ替える

  • Clean mixing bowl.: ボウルから全ての材料を消す

  • Pour contents of the mixing bowl into the baking dish.: ボウルの中の全ての材料を耐熱皿にコピーする

  • 動詞 the 材料名.: ループを始める
    ループでは以下を実行する
    1) 材料の値をチェック
    2-1) 0でなければ、"until" に達するまで処理が続く
    2-2) 0であれば、ループを抜けて "until" の後の命令を実行する

  • 動詞 [the 材料名] until 動詞(過去形?).: ループ区間の終わりを示す
    過去形 (?) の動詞は、ループ開始時の動詞と同じものを使う
    材料名が指定された場合、これが実行されるたびに材料の値を -1 する

  • Set aside.: 最も内側のループが終了し、"until" の後の命令が実行される

  • Serve with 補助レシピ.: 副料理長を呼び、補助レシピ (後述) を実行する

  • Refrigerate [for 数値 hours].: 処理を終了する
    補助レシピ内なら副料理長からボウルを受け取る
    時間が指定された場合は、耐熱皿の値を出力する

Method.
Sift the flour.
Put flour into mixing bowl.
Serve with caramel sauce.
Stir for 2 minutes.
Remove egg.
Rub the flour until sifted.
Stir for 2 minutes.
Fold the butter into the mixing bowl.
Pour contents of the mixing bowl into the baking dish.

提供

全ての耐熱皿が空くまで、一番上の値を取り出しては出力、を繰り返します。

記載は任意ですが、出力をしたい場合は必須です。

Serves 数値.

補助レシピ

特殊な材料 (ソースなど) を得るために必要な小さなレシピです。
補助レシピはメインのレシピの後に続けて書きます。

メインレシピのどこかで例えばソースを呼び出したとき、副料理長が料理長のボウルや耐熱皿の中身をコピーしてソースをつくり始めます。
副料理長の仕事は料理長のボウルなどには反映されませんが、調理が終わると1つ目のボウルの中身を料理長に渡します。


実行環境

Python でもいけるようなことが書いてありましたが、やり方わからず。

ブラウザ上で動かせるものがあったのでそちらを利用すると良いでしょう。

Chef | Esolang Park


コーディング

ここからは、実際にいろいろつくってみます。

Hello World!

Hello World Cake.

Tutorial of Chef.

Ingredients.
32 g butter
87 g flour
111 ml water
114 g egg
108 ml milk
100 g sugar
33 g salt

Method.
Put salt into the mixing bowl.
Put sugar into the mixing bowl.
Put milk into the mixing bowl.
Put egg into the mixing bowl.
Put water into the mixing bowl.
Put flour into the mixing bowl.
Put butter into the mixing bowl.
Serve with toppings.
Liquefy contents of the mixing bowl.
Pour contents of the mixing bowl into the baking dish.

Serves 1.

toppings.

Ingredients.
72 g cherry
101 g strawberry
108 g orange
108 g melon
111 g grape

Method.
Clean the mixing bowl.
Put grape into the mixing bowl.
Put melon into the mixing bowl.
Put orange into the mixing bowl.
Put strawberry into the mixing bowl.
Put cherry into the mixing bowl.
Refrigerate.

ケーキ本体とトッピングで分け、それぞれ「 World!」と「Hello」を出力するようにしてみました。
ここで、「Hello World Cake」はメインレシピ、「toppings」は補助レシピにあたります。

プログラム自体は文字に対応する数をそのまま材料の数量として、ひたすらボウルに入れた後に出力する、という単純なものです。


フィボナッチ数列

Fibonacci Chocolate.

Ingredients.
10 ml milk
1 chocolate

Method.
Put milk into the mixing bowl.
Put milk into the 2nd mixing bowl.
Remove chocolate from the mixing bowl.
Remove chocolate from the mixing bowl.
Fold milk into the mixing bowl.
Put chocolate into the mixing bowl.
Put chocolate into the mixing bowl.
Shake the milk.
Serve with butter.
Shake the milk until shake.
Fold milk into the 2nd mixing bowl.
Transfer the milk.
Fold chocolate into the mixing bowl.
Put chocolate into the 2nd mixing bowl.
Transfer the milk until transfer.
Pour contents of the 2nd mixing bowl into the baking dish.

Serves 1.

butter.

Ingredients.
1 ml milk
1 g salt

Method.
Fold milk into the mixing bowl.
Fold salt into the mixing bowl.
Clean the mixing bowl.
Put milk into the mixing bowl.
Add salt to the mixing bowl.

チョコレートのバター乗せみたいな変なものになりましたが、同時に milk 項分のフィボナッチ数列の出力ができるようになりました。
(初めの構想ではチョコレートが増殖する感じにしたかったのですが、結局バターが増殖するようになりました。)

流れ
初めにボウルに chocolate を2つ入れます。
ここで副料理長にバトンタッチし、牛乳を振りながらボウルから上2つを取り出しては足し、を繰り返して料理長に渡します。
これでフィボナッチチョコレートはほぼできているのですが、提供の順番は小さいものからにしたいので、2つ目のボウルを使って順番を逆にしています。

先程紹介したサイトではプログラムを段階的に進めることもできますので、どこで何が起こっているかわかりやすいかと思います。


まとめ

以上、Chef を触ってみました。

一風変わった言語シリーズの中では単純な方なのではないでしょうか。
ですが、if文がないようなのでできることは限られるかもしれません。
(こちら (If in esoteric programming language "chef") を見るとできなくはなさそうですが、どう実装すれば良いかわからず。)


「chef プログラミング言語」と調べるとサーバー構築 (?) かなんかの別の「Chef」がヒットしてしまい日本語の記事は見つけられなかったので、Chef やりたいけど英語やだという方はこの記事が助けになれば幸いです。
(英語の記事はたくさんありましたので問題ない方はそちらの方がいいかもしれません。)


最後まで読んでいただいてありがとうございました。また次回。

プログラミング言語「Haskell」を触ってみる

こんにちは。
今回は、プログラミング言語Haskell」を触ってみます。


最近は一風変わったプログラミング言語シリーズ (?) を更新していますが、そのネタを探す最中「ラムダ計算」というものに出会いました。
いろいろ歩き回っていたら「Haskell」という言語が出てきまして、ちょっと見てみたらおもしろそうだったので手を伸ばしてみることにしました。


目次


Haskell」とは

Haskell (Haskell Language) はプログラミング言語のひとつで、「関数型」に分類されます。

ちなみに、今までやってきた Processing や Python といった言語は「手続き型」に分類されるようです。
(このブログを開設してから2年以上経ちますが、このような分類は全く意識してきませんでした…)


実はおもしろそうだと思ったポイントはここで、杜甫々さんのサイト (とほほのHaskell入門 - とほほのWWW入門) を見たときに

「iには1が代入されていて... totalには100が代入されていて...」といった変数状態を考えながら、コンピューター目線でデバッグするのが手続き型。変数状態を意識することなく、純粋に「定義は正しいか」でデバッグできるのが関数型言語の優位性です。

とあった部分に興味を持ったのでした。


仕様に関しては、有名な言語なので日本語の記事もたくさんあるでしょうしここに書く必要もないと思います。
(私はとりあえず杜甫々さんのサイトを読んで、適宜補完しながら進めることにしました。)


実行環境

VSCode

VSCodeHaskell拡張機能があります。まずはそれを入れましょう。

説明欄の「Setup」の項にHaskellインストーラーである GHCupのリンク (GHCup) があります。
開くと Windows 用のコマンドが書いてありますので、PowerShell でこれを実行しましょう。

後は指示通りにインストールを進めていき、Installation - GHCup に従ってパスを通すなどしたら完了です。
(ここで苦戦したのですが、何回かやっていたらうまくいったので結局原因はわからず。)


ブラウザで

実はブラウザ上で動かせるものがあります。
(インストールを終えて「First Steps」を読み終えたところでさらっと紹介されるので、もっと早く言えよと思ったり。)

play.haskell.org


コーディング

ここからは、実際にいろいろつくってみます。

Hello World!

まずはおなじみの「Hello World!」を出力してみます。
ついでに、Haskell での実行までの流れを見てみましょう。


適当に「test.hs」というファイルをつくり、以下のように書きます。

main = putStrLn "Hello World!"

そして、ターミナルで

$ runghc test.hs

と実行すると、「Hello World!」が出力されます。


$ ghc test.hs

とすればコンパイルが行われ、exeファイル他が作成されます。

$ ./test

で実行可能です。


ちょっとした解説
(杜甫々さんのサイトを読めばわかりますが)

main はエントリポイントとよばれる、実行時に最初に読まれる部分です。
(Processing でいう setup() と同じ?)

putStrLn は文字列を改行付きで出力する関数です。
引数を書くときは空白を空けるだけでも良いし、括弧 () をつけても良いし、$ を挟んでも良いです。
($ は同じ行にあるそれ以降の記述を () に入れたのと同じ効果があるとのこと。)


ちなみに、

$ ghci

で対話的に実行可能です。
(Ctrl+d を押すか、ターミナルで :quit:q を実行すれば抜けられます。)


フィボナッチ数列

単純な方法
fib :: Int -> Int
fib 0 = 0
fib 1 = 1
fib n = fib (n-2) + fib (n-1)

main = print $ fib 20


Int型の引数を受け取ってInt型の値を返す、という意味で fib :: Int -> Int と宣言を行っています。
(型推論可能なので無くても動きはしますが、型を明確にしたい場合などに。)

関数は、左辺に関数名と引数、右辺に計算したい式を書くことで定義できます。
また、パターンマッチにより引数の値によって別々の定義にすることができます。

フィボナッチ数列の定義をそのまま書いたような感じですが、これを実行すると 6765 と正しい値が出力されました。


しかし、引数の値が大きくなってくると遅さが目立ちます (50項くらいで限界)。


メモを使った方法

いつだったか触れた「メモ化」を使ってみます。

fib :: Int -> Integer
fib 0 = 0
fib 1 = 1
fib n = fiblist !! (n-2) + fiblist !! (n-1)

-- メモの作成
fiblist :: [Integer]
fiblist = map fib [0..]

main = print $ fib 10000


ここで、Int は固定長整数、Integer多倍長整数です。
固定長整数は有限 (30 bit以上) の整数ですが、多倍長整数は理論上無限まで扱うことができます。
Int型で計算を行ってしまうと、あるところでオーバーフローが起こって計算結果が正しくなくなってしまいますので、それを解決するために無限まで扱えるInteger型を使います。
(わざわざ宣言しなくても、勝手にInteger型で計算してくれるようですが。)


fiblist !! (n-2)fiblistn-2 番目の要素を参照する、という意味です。


fiblist の定義の部分は

fiblist = [fib x | x <- [0..]]

と書くこともできます。
どちらも意味するところは同じで、[0..] で 0 から無限までの整数の配列をつくり、要素を1つずつ関数 fib にあてはめていっています。

無限長のリスト…?と思われるかもしれませんが、Haskell では「遅延評価」が採用されているので、必要なところだけしか考えません。
つまり、本来は無限まであるはずだけど、計算においては fiblist の引数で与えられた数までしか考えない、ということになります。
(たいていの言語は遅延評価の逆である「正格評価」を採用しています。)


この方法で5万項くらいまでそれなりの速さで計算できるようになりました。


zipWith を使った方法

こんな書き方もできるようです (Haskellでフィボナッチ数列 〜Haskellで非実用的なコードを書いて悦に入るのはやめろ〜 #Haskell - Qiita)。

fib :: [Integer]
fib = 0 : 1 : zipWith (+) fib (tail fib)

main = print $ fib !! 10000


ここでは fib をリストとして定義しています。
Haskell では [1 2 3]1:[2 3]1:2:[3]1:2:3:[] は同じ意味となっているので、上のような記述だと1つ目を 0、2つ目を 1、それ以降を zipWith (+) fib (tail fib) とするリスト、という意味になります。


zipWith は、1つ目に関数、2, 3つ目にリストを受けとる関数で、2つのリストの各要素について1つ目として与えた関数を適用してくれます。
つまり、fibtail fib の各要素の和 (+) を計算してくれる、ということになります。
tail は引数のリストの先頭を除いたリストを返す関数ですので、zipWith の部分ではまさしくフィボナッチ数列の定義に従った計算を行っていることになるのです。


この方法で100万項くらいまで広がりました。


ちなみに zipWith は、

zipWith f (x:xs) (y:ys) = f x y : zipWith f xs ys

こんな処理をしているようです。
(x:xs)(y:ys) は、対応するリストの先頭を xy に、残りを xsys に入れる処理です。
先程した説明のままといえばそうですね。


zipWith (改) を使った方法

実は、遅延評価が悪さ (?) をして遅い (Haskellの神話 - あどけない話) ようで、zipWith

zipWith' f (x:xs) (y:ys) = z `seq` (z : zipWith' f xs ys)
  where z = f x y

のように定義した zipWith' を使うとさらに高速化できるとのこと。

where は補助的な定義を行う際に使われます。

また、'seq' は正格評価を行う関数で、x 'seq' y の形で「x を評価した後に y を評価する」という意味になります。

つまり、zipWith では遅延評価のためいつまでも f x y が残ったままでしたが、zipWith' では f x y を評価して z としてから次に進むようになっています。
(これでなぜ早くなるのかはよくわかりません。)


変更の結果、200万項くらいまで許容できるようになりました。


まとめ

以上、Haskell を触ってみました。

軽い気持ちでフィボナッチ数列に手を出したら思ったより深かったので、一旦これだけで終わりにすることにしました。

まだ最初も最初の部分だとは思いますが実際触ってみても結構おもしろい言語だったので、今後続編が出るかもしれません。


最後まで読んでいただいてありがとうございました。また次回。

プログラミング言語「Shakespeare」を触ってみる

こんにちは。
今回は、プログラミング言語Shakespeare」を紹介します。


一風変わったプログラミング言語シリーズ (?) の3つ目です。

プログラミング言語「Piet」を使ってみる - プログラミングの備忘録

プログラミング言語「Uiua」を使ってみる - プログラミングの備忘録


目次


Shakespeare」とは

Shakespeare (The Shakespeare Programming Language) はプログラミング言語のひとつで、オースルンドさんとハッセルストロームさんが開発されたそうです。
(どうでもいいですが、名前に Å や ö が入っていて the 北欧という感じがして良いです。)

Shakespeare という名前はあのウィリアム・シェイクスピアに由来しており、「コードがシェイクスピアの演劇に見えるように」というコンセプトがあるとのこと。

割と自由度が高く「演劇」という体をとっているので、文才というかセンスが問われるような気がします。(私にはありません。)


仕様

The Shakespeare Programming Language の内容を要約していきます。


演目

1文字目から最初のピリオドまでがタイトルとみなされます。
特別意味があるわけではなく、装飾のようです。


登場人物

続いて、登場人物を羅列します。
一人一人が変数としての役割を持つので、型宣言と似たような意味合いでしょうか。
(全て整数型のようですが。)

名前とその説明で構成されますが、説明文は無視されます。
名前は実際にシェイクスピアの作品に登場する人物でないといけないようです。

Romeo, a young man with remarkable patience.


幕と場

演劇を場面毎に分ける、幕 (act) と場 (scene) があります。
この前後で登場人物の入退場 (後述) を実行します。

各幕、場はローマ数字で番号付けし、その後に場面の説明が続きます。
(例のごとく、説明文は無視されます。)

                    Act I: Hamlet's insults and flattery.
                    Scene I: The insulting of Romeo.

装飾的な意味もありますが、goto文 (後述) で飛ぶためのラベルとしても機能します。


登場と退場

登場人物がステージ上にいないと発言できません。

Enter で入場、Exit で一人の退場、Exeunt で複数人の退場が可能です。
Exeunt のみで使うと、全員退場します。

[Enter Hamlet and Romeo]
[Exit Romeo]
[Exeunt Ophelia and Hamlet]
[Exeunt]


発言

プログラムの本文にあたります。
「人物名: 発言内容」の形で記述していきます。

Hamlet:
 You lying stupid fatherless big smelly half-witted coward!
 You are as stupid as the difference between a handsome rich brave
 hero and thyself! Speak your mind!

(お分かりの通り、発言内容はちゃんと理解できるようなものにはなっていません。)


定数値について

各名詞がその良悪で 1 または -1 の値を持っています。
例えば、flowerhero は 1、pigcoward は -1 です。

名詞の前に形容詞を置くことで、値を2倍にできます。
例えば、lying stupid fatherless big smelly half-witted coward では形容詞6つに -1 の名詞なので、$-64$ ($= 2^{6} \times (-1)$) を意味します。


演算について

足し算は the sum of X and Y
引き算は the difference between X and Y
かけ算は the product of X and Y
割り算は the quotient between X and Y
(ただし、整数型のみのため小数点以下切り捨て)

2乗は the square of X
3乗は the cube of X


値の代入について

値は話しかけられている人物に代入されます。
例えば、HamletRomeo のみが登場している場面で

Hamlet:
 You lying stupid fatherless big smelly half-witted coward!

とあったら、you にあたる Romeolying stupid fatherless big smelly half-witted coward ($= -64$) が代入されます。

同じ状況で

Hamlet:
 You lying stupid fatherless big smelly half-witted coward!
 You are as stupid as the difference between a handsome rich brave
 hero and thyself!

であった場合、1文目で Romeo に $-64$ が代入され、2文目で handsome rich brave hero ($= 8$) から thyself にあたる Romeo が減算されます。
つまり、Romeo が $72$ になります。
(as 形容詞 as の形も代入を意味します。)


出力について

open your heartspeak your mind の2種類があります。
前者では話しかけられている人物の値を数字で出力、後者では文字で出力されます。
例えば、先程の状況で

Hamlet:
 You lying stupid fatherless big smelly half-witted coward!
 You are as stupid as the difference between a handsome rich brave
 hero and thyself! Speak your mind!

となると、ASCIIコードで $72$ に対応する「H」が出力されます。


入力について

listen to your heartopen your mind があります。
前者では数を、後者では文字を対応する数に変換して受け取ります。


goto文について

let us return to scene III のような発言があった場合、指定された場に移動できます。
let us の代わりに we shallwe mustreturn to の代わりに proceed to を使えます。
(scene 間の移動は同じ act の中でのみ可能です。)


if文について

何らかの疑問文で条件を指定し、受け手の if soif not とその後の発言で、真偽によって何を実行するかを決めます。
例えば、

Juliet:
 Am I better than you?

Hamlet:
 If so, let us proceed to scene III.

なら、JulietHamlet よりも大きかった場合 scene III へ飛びます。


比較について

is X as 形容詞 as YX == Y
is X better than YX > Y
is X worse than YX < Y
を意味します。

否定を考えたければ、not をつければ良いです。


スタック

登場人物が覚えられる整数は1つだけはなく、整数型の箱とスタックの2つを持っています。
値のプッシュは remember で、ポップは recall で可能です。
例えば、

Romeo:
 Open your mind! Remember yourself.

とあれば、話しかけられた人物は入力を受け取り、その値をスタックに入れます。

Romeo:
 Recall your unhappy childhood. Speak your mind!

とあれば、スタックからひとつ取り出し、対応する文字に変換して出力します。
(recall your の後に続く単語は装飾で、特に意味はありません。)


実行環境

コマンドプロンプト

いろいろ書いてあります。

The Shakespeare Programming Language

Write code inspired by Shakespeare with esolang | Opensource.com

コンパイラのインストール

が、ややこしそうだったのでやめておきました。


python のパッケージとして「shakespearelang」というものもありました。

コマンドプロンプト

pip install shakespearelang

を実行すればインストールされます。
(あらかじめ python を入れておく必要がありますが。)


実行にあたっては、コードファイルのあるフォルダでコマンドプロンプトを起動し

shakespeare run ファイル名.spl

と打ち込むことで実行結果が表示されるはずです。

しかし、「readline」モジュールが足りないと言われ、インストールしようとしても windows に対応していないみたいなことを言われてしまったので、結局諦めました。


ブラウザで

探したらありました。

Home | Shakespeare

Shakespeare | Esolang Park


まとめ

以上、Shakespeare を紹介しました。
解説を書いていたらそれで力尽きました。

パッと見は難解ですが、仕組みがわかってしまえば意外と読めます。
むしろ適宜ラベルをつけていくのでやりようによっては可読性が高い方…?

コーディングもやってみましたが、私は凝り性みたいな性分があるのか、ちゃんと成立した文にしようと考えてしまってやたら時間がかかってしまったので、あまりこの手の言語は向いていないかもと思いました。


最後まで読んでいただいてありがとうございました。また次回。

processingの備忘録 -逆高速フーリエ変換-

こんにちは。
今回は「逆高速フーリエ変換」を processing で実装してみます。


なかなか長編になってきました、フーリエ変換シリーズです。

processingの備忘録 -離散フーリエ変換- - プログラミングの備忘録

processingの備忘録 -逆離散フーリエ変換- - プログラミングの備忘録

processingの備忘録 -高速フーリエ変換- - プログラミングの備忘録

フーリエ変換を利用して絵を描く - プログラミングの備忘録


音声ファイルを扱ったときには、おまけとして音声のフーリエ変換を試してみるということもしてみました。

processingの備忘録 -音声- - プログラミングの備忘録


先日「逆離散フーリエ変換」を実装し、締めの部分で「逆変換の高速化はまだ先になりそうです」と書きました。
が、意外と簡単にできそうだったので「逆高速フーリエ変換」も扱ってみます。
(これに伴い、順変換の記事も理解が不十分だった部分を若干補充しました。)


目次


理論

高速フーリエ変換は過去にやりましたので、その逆も同じように考えていけばいいはずです。
数式自体は似ているので、工夫すれば一から考えなくても良いかもしれません。

しかし、(こう言っては失礼ですが) 面倒です。


一度、フーリエ変換の式を見てみましょう。

離散フーリエ変換

$$ F(f) = \displaystyle \sum _{t = 0} ^{N - 1} f(t) e ^{ -\frac{2 \pi i f t}{N} } $$

逆変換

$$ f(t) = \displaystyle \frac{1}{N} \sum _{f = 0} ^{N - 1} F(f) e ^{ \frac{2 \pi i f t}{N} } $$

逆変換の式で右辺全体の複素共役をとってみます。
同時に、等しくなるように細かな部分をいじります。

$$ f(t) = \displaystyle \frac{1}{N} \overline{ \sum _{f = 0} ^{N - 1} \overline{F(f)} e ^{ -\frac{2 \pi i f t}{N} } } \\ $$

すると、$F(f)$ の複素共役フーリエ変換複素共役を $N$ で割ったものが逆変換に等しい、ということがわかりました。

つまり、わざわざ逆変換を考える必要はなく、既にあるフーリエ変換の関数を使い回すことができるのです。
(逆離散フーリエ変換でも同じ考え方ができるはずですが、記事更新当時は知らなかったので数式をそのまま実装していました。)


普通に Wikipedia に書いてありました。
高速フーリエ変換 - Wikipedia


実装

というわけで、実装してみます。

Complex[] IFFT(Complex[] input){
  int N = input.length;
  Complex[] output = new Complex[N];

  // F(f) の複素共役
  for(int i = 0; i < N; i ++){
    input[i] = input[i].conjugate();
  }

  // 高速フーリエ変換
  output = FFT(input);

  // 変換後の値の複素共役を N で割る
  for(int i = 0; i < N; i ++){
    output[i] = (output[i].conjugate()).cdiv_scalar(N);
  }

  return output; // f(t) の出力
}


ここでは、複素数のクラスを使っています。
(高速フーリエ変換のときとは少し違っているので気をつけてください。)

class Complex{
  float re, im;
  
  Complex(float _re, float _im){
    re = _re;
    im = _im;
  }
  
  Complex cadd(Complex a){
    return new Complex(re+a.re, im+a.im);
  }
  
  Complex csub(Complex a){
    return new Complex(re-a.re, im-a.im);
  }
  
  Complex cmult(Complex a){
    return new Complex(re*a.re-im*a.im, re*a.im+im*a.re);
  }

  Complex cmult_scalar(float a){
    return new Complex(re*a, im*a);
  }
  
  Complex cdiv(Complex a){
    float b = a.re*a.re + a.im*a.im;
    return new Complex((re*a.re-im*a.im)/b, (-re*a.im+im*a.re)/b);
  }

  Complex cdiv_scalar(float a){
    return new Complex(re/a, im/a);
  }
  
  float cabs(){
    return sqrt(re*re + im*im);
  }
  
  Complex cexp(){
    return new Complex(exp(re)*cos(im), exp(re)*sin(im));
  }

  Complex conjugate(){
    return new Complex(re, -im);
  }
}


試用

20 Hzの正弦波と70 Hzの余弦波の合成波で試してみました。

上から、変換前、変換後、逆変換後、となっています。

変換後は絶対値と実部、虚部をそれぞれ描画しています。
ピークのある位置は20 Hzと70 Hzで、合っているようです。

これを逆変換して、実部と虚部をそれぞれ描画しています。
実部 (赤) は元の形に戻っており、虚部 (青) はほとんどないことがわかります。


どうやら合っていそうな描画となっており、これにて逆高速フーリエ変換の実装完了です。
ちなみに、計算速度は離散化しただけのものよりもかなり速くなっていました。


まとめ

以上で、逆高速フーリエ変換を processing で実装することができました。

高速にできたので、そのうち二次元に拡張して画像処理に使ったり、スペクトログラムをつくってみたりという記事も書くかと思います。
お楽しみに。


最後まで読んでいただいてありがとうございました。また次回。

processingの備忘録 -逆離散フーリエ変換-

こんにちは。
今回は「逆離散フーリエ変換」を processing でできるようにしてみようと思います。


これまでに、フーリエ変換シリーズとして何記事か書いてきました。

processingの備忘録 -離散フーリエ変換- - プログラミングの備忘録

processingの備忘録 -高速フーリエ変換- - プログラミングの備忘録

フーリエ変換を利用して絵を描く - プログラミングの備忘録


音声ファイルを扱ったときには、おまけとして音声のフーリエ変換を試してみるということもしてみました。

processingの備忘録 -音声- - プログラミングの備忘録


というわけで、フーリエ変換逆変換に触れてみようというのが今回の目的です。
中でも、プログラミング向きであろう「逆離散フーリエ変換」というものを processing に実装してみます。


目次


逆離散フーリエ変換とは

その名の通り、離散フーリエ変換です。

フーリエ変換では時間領域のデータが周波数領域のデータに変換されましたが、逆変換では周波数領域のデータが時間領域のデータに変換できます。
つまり、こんな周波数の波が合わさってできた音です、という情報から実際の音の波形を復元できる、といったところでしょうか。


一般的なフーリエ変換は以下のように表されます。

$$ F(f) = \displaystyle \int _{-\infty} ^{\infty} f(t) e ^{-2 \pi i f t} $$

これを離散化すると離散フーリエ変換です。

$$ F(f) = \displaystyle \sum _{t = 0} ^{N - 1} f(t) e ^{ -\frac{2 \pi i f t}{N} } $$

ここで、$N$ はデータの個数、$i$ は虚数単位、$t$ や $f$ は任意の実数で、それぞれ時間と周波数の次元を持ちます。


この逆ということですが、$F(f)$ を入れると $f(t)$ を返してくれるような関数になります。

$$ f(t) = \displaystyle \frac{1}{N} \sum _{f = 0} ^{N - 1} F(f) e ^{ \frac{2 \pi i f t}{N} } $$

これが逆離散フーリエ変換です。
導出に関しては省略しますが、割とあっさり求められるようです。

得られた $f(t)$ は複素数となると予想されますが、フーリエ変換前の $F(f)$ が実数値だった場合、虚部は無視できるということになります。


実装

改めて、逆離散フーリエ変換の式を見てみます。

$$ f(t) = \displaystyle \frac{1}{N} \sum _{f = 0} ^{N - 1} F(f) e ^{ \frac{2 \pi i f t}{N} } $$

指数の部分に虚数単位が入っており、このままでは計算しづらいです。
そこで、あのレオンハルト・オイラーの名を冠した、

$$ e ^{\pm i \theta} = \mathrm{cos} \theta \pm i \mathrm{sin} \theta $$

という数式を利用して、実部と虚部に分けます。

$$ \begin{align} f(t) &= \displaystyle \frac{1}{N} \sum _{f = 0} ^{N - 1} F(f) \left( \mathrm{cos} \left( \frac{2 \pi f t}{N} \right) + i \mathrm{sin} \left( \frac{2 \pi f t}{N} \right) \right) \\ &= \displaystyle \frac{1}{N} \sum _{f = 0} ^{N - 1} F(f) \mathrm{cos} \left( \frac{2 \pi f t}{N} \right) + i \displaystyle \frac{1}{N} \sum _{f = 0} ^{N - 1} F(f) \mathrm{sin} \left( \frac{2 \pi f t}{N} \right) \end{align} $$

ここで、離散フーリエ変換の式を見てみます。

$$ F(f) = \displaystyle \sum _{t = 0} ^{N - 1} f(t) \mathrm{cos} \left( \frac{2 \pi f t}{N} \right) - i \displaystyle \sum _{t = 0} ^{N - 1} f(t) \mathrm{sin} \left( \frac{2 \pi f t}{N} \right) $$

符号や $N$ で割るかが違うくらいで、残りはほとんど同じです。

ということで、離散フーリエ変換のときにつくった関数を割と使えます。
ただし受け取る値 $F(f)$ は複素数なので、実部と虚部それぞれについて計算しなければいけません。

$F(f) = \mathrm{Re} + i \mathrm{Im}$ とおくと、

$$ \begin{align} f(t) &= \displaystyle \frac{1}{N} \sum _{f = 0} ^{N - 1} (\mathrm{Re} + i \mathrm{Im}) \mathrm{cos} \left( \frac{2 \pi f t}{N} \right) + i \displaystyle \frac{1}{N} \sum _{f = 0} ^{N - 1} (\mathrm{Re} + i \mathrm{Im}) \mathrm{sin} \left( \frac{2 \pi f t}{N} \right) \\ &= \displaystyle \frac{1}{N} \sum _{f = 0} ^{N - 1} \left( \mathrm{Re} ~ \mathrm{cos} \left( \frac{2 \pi f t}{N} \right) - \mathrm{Im} ~ \mathrm{sin} \left( \frac{2 \pi f t}{N} \right) \right) + i \frac{1}{N} \sum _{f = 0} ^{N - 1} \left( \mathrm{Im} ~ \mathrm{cos} \left( \frac{2 \pi f t}{N} \right) - \mathrm{Re} ~ \mathrm{sin} \left( \frac{2 \pi f t}{N} \right) \right) \end{align} $$

と変形できます。

長くなりましたが、これをプログラムにすると以下のようになります。
(inverse discrete Fourier transform の略で IDFT です。)

void idft(){
  float[] real = new float[N];
  float[] imag = new float[N];
  for(int i = 0; i < N; i ++){
    real[i] = 0;
    imag[i] = 0;
    for(int j = 0; j < N; j ++){
      real[i] += (re[j] * cos(2 * PI * f[j] * t[i] / N) - im[j] * sin(2 * PI * f[j] * t[i] / N)) / N;
      imag[i] += (im[j] * cos(2 * PI * f[j] * t[i] / N) + re[j] * sin(2 * PI * f[j] * t[i] / N)) / N;
    }
    
    f[i] = real[i];
  }
}


前述の通り、逆フーリエ変換後の $f(t)$ は実部だけ考えれば良いので f[i] = real[i]; となっています。


実際に変換してみる

これにて逆離散フーリエ変換ができるようになったので、何らかの波をフーリエ変換した後、逆フーリエ変換で元に戻るかを試してみます。

フーリエ変換する波 signal は、40 Hzの正弦波と50 Hzの余弦波の合成波にしてみました。

signal[i] = cos(40 *2*PI*t[i]/N) + sin(50 *2*PI*t[i]/N);


実行自体は簡単で、波の情報を入れておいた signal に対して dft() を実行し、その結果 reim に対して idft() を実行する、という感じです。

描画に関しては、signal (変換前) と sqrt(re*re+im*im) (フーリエ変換後)、f (逆変換後) を良い感じに図示するようにします。

結果、

ここでは上から順に signalsqrt(re*re+im*im)f となっています。
見てみると、一番上と一番下の波形が同じ形になっており、逆変換によって周波数の情報から波形を復元することができました。
(ちなみに、真ん中の図でシグナルが見えている位置を出力するとちゃんと40 Hzと50 Hzになっていました。)


実際の音声処理がどうかは知りませんが、フーリエ変換後のシグナルから特定の周波数を除いたりして逆変換をすると、音声の編集 (?) ができます。
振幅の小さい成分を除くとか、逆に加えることもできると思います。


まとめ

以上で、逆離散フーリエ変換を processing で実装することができました。
(あまり N が大きいと変換に時間がかかってしまいますが…)

これを二次元に拡張すれば画像の処理ができたり、スペクトログラムというものがつくれたりとまた新しいことができそうですので、今後記事を書くかと思います。


また、逆フーリエ変換にも高速なものがあるようですが、順の高速フーリエ変換も結局ちゃんと理解できてないような感じ (記事を書いておきながら) なので、こちらはまだまだ先になりそうです。


最後まで読んでいただいてありがとうございました。また次回。

processingの備忘録 -ASCIIアート-

こんにちは。
今回は、processing で「ASCIIアート」をつくります。


目次


ASCIIアートとは

ASCIIアートは、文字で描かれた絵のことを指し、略称として「AA」とよばれることもあります。
2ちゃんねるニコニコ動画などでよく見られるので、聞いたり見たりしたことがあるという方もいるのではないでしょうか。

そもそも「ASCII」とは文字コードの一種で「American Standard Code for Information Interchange」の頭字語になっています。
今では「Unicode」が主流になっていてASCIIにない文字が使われたりもしていますが…


ASCIIアートには「:D」や「(´・ω・`)」、「orz」のような一行に収まるものも、複数行にわたるものもあります。
「人生オワタの大冒険」や以前取りあげた「Stone Story RPG」(「Stone Story RPG」で遊んでみる - プログラミングの備忘録) などはASCIIアートで描かれたゲームとして有名かと思います。


実装

流れとしては、

  1. 画像の読み込み
  2. グレースケール化
  3. 文字列に変換
  4. 出力

という感じです。


画像の読み込み

まずは、ASCIIアートにしたい画像を用意します。
私のアイコンの元になっている、こちらの画像でやってみることにしました。


画像を読み込む方法については、過去に取りあげています。

taq.hatenadiary.jp


読み込み自体は1行でおしまいです。

PImage img = loadImage("image.png");


グレースケール化

元の画像そのままではカラーになっていて直接文字に変換するのが難しいので、グレースケールに変えます。
グレースケールでは画像の色が黒 (0) から白 (255) の256段階で表されるようになります。


processing の色には大きく RGB と HSB の2つがあります。

RGB なら各値の平均を取れば良いでしょう。
(単純な平均以外にも様々やり方があるようですが。グレースケール画像のうんちく #rgb - Qiita)

float average = (red(col) + green(col) + blue(col)) / 3;


HSB では明度の情報のみを使えば良いので、 brightness() で明度を取得すれば良さそうです。

float brightness = brightness(col);


ここで、col は画像中のある点での色の情報を持った変数で、例えば画像 img の座標 (i, j) での色を取得したいときは以下のようにします。

color col = img.get(i, j);


文字列に変換

画像の各点について明度を取得し、その値に対応した文字に置き換えた配列をつくる、ということを考えます。
変換に使う文字は、Shiffman先生のものを参考にさせていただきました。

String letters = "N@#W$9876543210?!abc;:+=-,._  "; // 左に行くほど明るい部分に対応


(i, j) における brightness (or average) の値を letters の文字と対応させた後、対応する文字を letters から取り出して配列 ascii に入れていきます。

int index = floor(map(brightness, 0, 255, 0, letters.length()-1));
ascii[i][j] = letters.charAt(index);


出力

以上でASCIIアートの素材をつくることができたので、これを描画します。


キャンバスに描くのであれば、text() を使って文字を描画します。

textSize(2);
text(ascii[i][j], i*2, j*2);

ただし、文字の大きさ倍だけキャンバスのサイズも大きくなってしまうので工夫が必要です。

あと、本当に文字でできているのか確認ができない。


追記

一応、letters = "NOo+^*-. "; としてみたり、表示する文字を間引いてみたりして、それなりに見えるようにできました。

粗くとったときは letters も粗く変化させた方が良いということですね。

以上


コンソールに描くのであれば、print() でひたすら出力します。

for(int j = 0; j < img.height; j ++){
  for(int i = 0; i < img.width; i ++){
    print(ascii[i][j]);
  }
  print("\n");
}

しかし、横幅が十分長くないと途中で改行されてしまい、正しく表示されません。
そこで、テキストファイルとして出力することとします。


テキストファイルの出力方法は過去に取りあげました。

taq.hatenadiary.jp


createWriter() を使う方法だと構造を変えなくて良いので楽です。

PrintWriter file = createWriter("text.txt"); // text.txt を作成
for(int j = 0; j < img.height; j ++){
  for(int i = 0; i < img.width; i ++){
    file.print(ascii[i][j]);
  }
  file.print("\n");
  file.flush(); // テキストファイルに書き込み
}
file.close(); // ファイルを閉じる


文字サイズを小さくしたり頑張って縮小したりしても全貌が見られなかったので、書き込む文字を少し間引きました。
(フォントは等幅のもの (MSゴシックなど) を使う必要があることに注意です。)

拡大すると、ちゃんと文字で構成されていることがわかります。


まとめ

以上、processing でASCIIアートをつくることができるようになりました。

今回は画像に対して変換を行いましたが、動画やカメラからの入力に対しても同様にできるはずです。
また、明度によって文字を変えていきましたが、画像の区切り方を変えれば線の向きなどにも対応した、より実際のASCIIアートに近いものもつくることができるのではないかと思います。
機会があればまた記事にするかもしれません。


最後まで読んでいただいてありがとうございました。また次回。


参考