DhallでFizzBuzzしよう


設定ファイルのアーミーナイフ的言語 Dhall

Dhallは便利な言語ですが、再帰を行う関数を許容しないため、フィルターなどを書くときに一癖あります。

書く時のコツをFizzBuzzを例に紹介したいと思います。

FizzBuzzは3の倍数の時Fizz、5の倍数の時Buzz両方に一致するとき(15の倍数の時)FizzBuzzと表示する、言語の文法解説などで使われるプログラムです。

Dhallで面倒なのは倍数を確認する部分で、標準的な機能のPreludeには割り算や余りを求める関数がありません。そのため実現には工夫必要になります。

データを考える

プログラムで表現する状態は、Fizz、Buzz、FizzBuzz、普通の数字の4つです。HaskellやPureScriptのデータ型はUnionと呼ばれる方法で表現します。ちなみにV10から表現方法が変化しましたので、昔のものをコピペしても動かないことがあります。

let FBdata : Type = <Fizz : Natural | Buzz : Natural | FizzBuzz : Natural | Normal : Natural>

鍵かっこで囲っている以外はHaskellライクなので、見慣れた感じです。

最終的に文字列になるので、文字列に変換する関数を定義します。

let FBdata/show
      = λ(d : FBdata)
       merge
          { Fizz = λ(t : Natural)  "Fizz"
          , Buzz = λ(t : Natural)  "Buzz"
          , FizzBuzz = λ(t : Natural)  "FizzBuzz"
          , Normal = λ(n : Natural)  Natural/show n
          }
          d

mergeは、組込み関数として使用でき、パターンマッチを書きたいときに使用します。case of構文の代わりぐらいの認識ですが、もっと面白い使い方の記事とかがあれば私が喜びます。

倍数チェック

プログラムの重要な部分である倍数をチェックする部分です。戦略としてはチェックする数字がFizzBuzzの倍数になっているかを見るのではなく、3の倍数及び5の倍数のListを必要なだけ作成して、チェックする数字がListに含まれるかを確認します。

まず、等差数列を作成する関数を書きます。

let Prelude = https://raw.githubusercontent.com/dhall-lang/dhall-lang/v10.0.0/Prelude/package.dhall

let numlist
      = λ(n : Natural)
       λ(pitch : Natural)
       Prelude.List.generate n Natural (λ(x : Natural)  x * pitch)

最新のPreludeはコンパイルが通らなかったので、V10のものを指定しています。

チェック関数は、1つの数字を渡してその数字が、Fizzリスト、Buzzリストに含まれるかを確認します。両方に該当する場合、FizzBuzzとして値を返します。

let check
      = λ(n : Natural)
       let fizzy = Prelude.List.any Natural (Prelude.Natural.equal n) (numlist n 3)
        let buzzy = Prelude.List.any Natural (Prelude.Natural.equal n) (numlist n 5)
        let checked =
            if    fizzy && buzzy then FBdata.FizzBuzz n
            else  if fizzy && Prelude.Bool.not buzzy then FBdata.Fizz n
            else  if Prelude.Bool.not fizzy && buzzy then FBdata.Buzz n
            else  FBdata.Normal n
        in checked

確認のたびにnまでのFizzリスト、Buzzリストを生成しているので、巨大な数を確認する場合は、ほかの方法をとったほうが良いと思います。

出力

今までの関数を繋げて完了です。必要なだけの長さのリストを渡して、各要素ごとにチェック関数を適用させ、FBdataのリストを作成します。FBdata/showを通して、文字列の読める形にします。

in  let fizzbuzz = Prelude.List.map Natural FBdata check (numlist 20 1)
    in  Prelude.List.map FBdata Text FBdata/show fizzbuzz

すべてまとめて以下のようになります。

fizzbuzz.dhall
let Prelude = https://raw.githubusercontent.com/dhall-lang/dhall-lang/v10.0.0/Prelude/package.dhall

let FBdata : Type = <Fizz : Natural | Buzz : Natural | FizzBuzz : Natural | Normal : Natural>

let FBdata/show
      = λ(d : FBdata)
       merge
          { Fizz = λ(t : Natural)  "Fizz"
          , Buzz = λ(t : Natural)  "Buzz"
          , FizzBuzz = λ(t : Natural)  "FizzBuzz"
          , Normal = λ(n : Natural)  Natural/show n
          }
          d

let numlist
      = λ(n : Natural)
       λ(pitch : Natural)
       Prelude.List.generate n Natural (λ(x : Natural)  x * pitch)

let check
      = λ(n : Natural)
       let fizzy = Prelude.List.any Natural (Prelude.Natural.equal n) (numlist n 3)
        let buzzy = Prelude.List.any Natural (Prelude.Natural.equal n) (numlist n 5)
        let checked =
            if    fizzy && buzzy then FBdata.FizzBuzz n
            else  if fizzy && Prelude.Bool.not buzzy then FBdata.Fizz n
            else  if Prelude.Bool.not fizzy && buzzy then FBdata.Buzz n
            else  FBdata.Normal n
        in checked

in  let fizzbuzz = Prelude.List.map Natural FBdata check (numlist 20 1)
    in  Prelude.List.map FBdata Text FBdata/show fizzbuzz

実行させれば、ちゃんと実行できていることがわかります。

bash
> cat fizzbuzz.dhall | dhall-to-json
["0","1","2","Fizz","4","Buzz","Fizz","7","8","Fizz","Buzz","11","Fizz","13","14","FizzBuzz","16","17","Fizz","19"]

Dhallの関数を使用することで、手で書くには面倒な記載を自動化することができます。逆に関数で記載が難しいような場合には、べた書きしてしまえば良いため、導入の心理的な障壁も小さいです。いろいろな出力先があるため、ちょっとでも、一部だけでも、一回だけでも使ってみてください。

Twitterをやってますので、もし宜しければフォローしてみてください。@noolbar