• 作成:
  • 更新:

HaskellでBuilderパターンをやってMaybeをなるべく除去したい(型引数が複数ある場合)

  • 単一項ならbarbiesが使える
  • 複数項ならボイラープレートを書いていくしかない
  • Kindを潰す方法を思い出すのに時間がかかった

以下は悩んでた時のメモと、悩んだ人向けのインデックスです。

Maybeをなるべく消したい

Haskellで、 RustのちょっとやりすぎなBuilderパターン | κeenのHappy Hacκing Blog のようなことをしようとしました。

HaskellではKind概念があるからHKDで楽勝では?

import           Data.Functor.Identity

data Foo haveHoge
  = Foo
  { hoge :: haveHoge Int
  , huga :: Int
  } deriving Show

type MaybeHaveHogeFoo = Foo Maybe

maybeHaveHogeFoo = Foo Nothing 1

type IdentityHaveHogeFoo = Foo Identity

identityHaveHogeFoo = Foo (Identity 0) 1

残念ながらこれはエラーになります。

[1 of 1] Compiling Main             ( builder-pattern.hs, builder-pattern.o )

builder-pattern.hs:7:14: error:
    • No instance for (Show (haveHoge Int))
        arising from the first field of ‘Foo’ (type ‘haveHoge Int’)
      Possible fix:
        use a standalone 'deriving instance' declaration,
          so you can specify the instance context yourself
    • When deriving the instance for (Show (Foo haveHoge))
  |
7 |   } deriving Show
  |              ^^^^

haveHogeShowではないよと。そりゃそうである。

Kindであることを伝えてみる

まずhaveHogeはTypeではなくKindなんだよなあ。それを伝えてなかったらそりゃエラーにもなりますね。

修正しましょう。

{-# LANGUAGE KindSignatures #-}
import           Data.Functor.Identity
import           Data.Kind

data Foo (haveHoge :: Type -> Type)
  = Foo
  { hoge :: haveHoge Int
  , huga :: Int
  } deriving Show

type MaybeHaveHogeFoo = Foo Maybe

maybeHaveHogeFoo = Foo Nothing 1

type IdentityHaveHogeFoo = Foo Identity

identityHaveHogeFoo = Foo (Identity 0) 1

これもエラーになります。

[1 of 1] Compiling Main             ( builder-pattern.hs, builder-pattern.o )

builder-pattern.hs:9:14: error:
    • No instance for (Show (haveHoge Int))
        arising from the first field of ‘Foo’ (type ‘haveHoge Int’)
      Possible fix:
        use a standalone 'deriving instance' declaration,
          so you can specify the instance context yourself
    • When deriving the instance for (Show (Foo haveHoge))
  |
9 |   } deriving Show
  |              ^^^^

エラーの内容同じやんけ。

deriving instanceしてみる

まあここはちゃんとエラーメッセージが表示している推奨方法を一度試してエラーを見てみるべきですね。

{-# LANGUAGE KindSignatures     #-}
{-# LANGUAGE StandaloneDeriving #-}
import           Data.Functor.Identity
import           Data.Kind

data Foo (haveHoge :: Type -> Type)
  = Foo
  { hoge :: haveHoge Int
  , huga :: Int
  }

deriving instance Show a => Show (Foo a)

type MaybeHaveHogeFoo = Foo Maybe

maybeHaveHogeFoo = Foo Nothing 1

type IdentityHaveHogeFoo = Foo Identity

identityHaveHogeFoo = Foo (Identity 0) 1

公開当初deriving instance Show a => Show Foo aとカッコを忘れていたのは修正しました。

エラー内容は、

[1 of 1] Compiling Main             ( builder-pattern.hs, builder-pattern.o )

builder-pattern.hs:12:39: error:
    • Expected kind ‘* -> *’, but ‘a’ has kind ‘*’
    • In the first argument of ‘Foo’, namely ‘a’
      In the first argument of ‘Show’, namely ‘(Foo a)’
      In the stand-alone deriving instance for ‘Show a => Show (Foo a)’
   |
12 | deriving instance Show a => Show (Foo a)
   |                                       ^

まあKindにShowであることを期待したらそうなりますよね。

ついでにKindであることを伝えなくても暗黙に推論して同じようなエラーを投げてきます。

Kind自体がShowであるのではなくてKindの結果の型がShowである必要があるのでそうですね。

Kindを完全に潰したら一応は通るけど解決策ではない

どう書くんだよこれ、どこかでそう言う構文を見た気がするけど思い出せない。

型族とかはtype classでの問題になるし…

Haskellの種(kind)について (Part 2) - Haskell-jp の > ですが、私たちは完全に種推論に頼らないような種注釈を提供することで、T :: (k -> ) -> k -> というような種多相化された型コンストラクタを作ることができます: を参考にしてみましょう。

ダメですね。 ConstraintKindsでググって行くか。

さて、HListはShowのインスタンスにできるでしょうか。HListに登場する全ての型がShowのインスタンスであればできそうです。このように型への制限を表すのがConstraintで、それをカインドのレベルで扱えるようにするのがConstraintKinds拡張です。

Printf実装を通して学ぶGADTs, DataKinds, ConstraintKinds, TypeFamilies - Just $ A sandbox

おっこれそのものじゃないですか?

いやこれ手動でShowinstanceを記述するものですね… そりゃ手動で書けば行けるだろうけど、それはミスを誘発するからやりたくない…

よく分からなくなってきてシノニムで完全にKindを適応してTypeにしたらコンパイル通りました。

{-# LANGUAGE ConstraintKinds        #-}
{-# LANGUAGE DataKinds              #-}
{-# LANGUAGE FlexibleInstances      #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE KindSignatures         #-}
{-# LANGUAGE PolyKinds              #-}
{-# LANGUAGE RankNTypes             #-}
{-# LANGUAGE StandaloneDeriving     #-}
{-# LANGUAGE TypeFamilies           #-}
{-# LANGUAGE TypeSynonymInstances   #-}
import           Data.Functor.Identity
import           Data.Kind

data Foo m
  = Foo
  { hoge :: m Int
  , huga :: Int
  }

type MaybeHaveHogeFoo = Foo Maybe
deriving instance Show MaybeHaveHogeFoo

maybeHaveHogeFoo = Foo Nothing 1

type IdentityHaveHogeFoo = Foo Identity
deriving instance Show IdentityHaveHogeFoo

identityHaveHogeFoo = Foo (Identity 0) 1

main :: IO ()
main = do
  print maybeHaveHogeFoo
  print identityHaveHogeFoo

うーん実用上アプリケーション側ではこれで問題は無いのですが、気持ち悪いからどうにかしたい。

なんかライブラリ使えば書けるらしい

ですが安心してください。もちろん Haskell には barbies という便利なライブラリがあり、Generics の力によりボイラープレートを劇的に減らすことができます。

Higher-Kinded Data (HKD) について - Qiita

と言うか… これに答え書いてるじゃないか。

{-# LANGUAGE ConstraintKinds        #-}
{-# LANGUAGE DataKinds              #-}
{-# LANGUAGE FlexibleInstances      #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE KindSignatures         #-}
{-# LANGUAGE PolyKinds              #-}
{-# LANGUAGE RankNTypes             #-}
{-# LANGUAGE StandaloneDeriving     #-}
{-# LANGUAGE TypeFamilies           #-}
{-# LANGUAGE TypeSynonymInstances   #-}
{-# LANGUAGE UndecidableInstances   #-}
import           Data.Functor.Identity
import           Data.Kind

data Foo m
  = Foo
  { hoge :: m Int
  , huga :: Int
  }

deriving instance (Show (a Int)) => Show (Foo a)

type MaybeHaveHogeFoo = Foo Maybe

maybeHaveHogeFoo = Foo Nothing 1

type IdentityHaveHogeFoo = Foo Identity

identityHaveHogeFoo = Foo (Identity 0) 1

main :: IO ()
main = do
  print maybeHaveHogeFoo
  print identityHaveHogeFoo

これで動きます。フツーにKindを潰してやれば動いたわけですね… Kindのことを分かってあげられなかった。しばらく業務であんまりHaskell書いてなかったからHaskellの書き方を忘れてしまったのではと言う疑惑が自分に生まれてしまった。

ただしUndecidableInstances拡張を使っている、コワイ! 後、多分無駄な拡張残しまくってる。

前にHaskell Day行った時にこれ見たから素直にそこから導線辿れば良かった。当初はそんな難題だと思わなかったので普通に検索したら出てくる情報だと思ってたのですよね。

これで一見落着かと思いましたが、そうでもない。

複数のフィールドをそれぞれMaybeで包みたい

元々の目標として、複数のフィールドがそれぞれ埋まってるかどうかを型レベルで判定したいと言うのがありました。

barbiesは果たしてその期待に答えることが出来るのでしょうか。

AllBFに直接渡してやってもうまくいきませんでしたし、ドキュメント見ても2引数以上の型引数の対応はよく分かりませんね… 適応したい型自体はそんなに多くないわけなので、愚直に対応しましょう。

{-# LANGUAGE DataKinds            #-}
{-# LANGUAGE DeriveAnyClass       #-}
{-# LANGUAGE DeriveGeneric        #-}
{-# LANGUAGE FlexibleContexts     #-}
{-# LANGUAGE StandaloneDeriving   #-}
{-# LANGUAGE TypeApplications     #-}
{-# LANGUAGE TypeFamilies         #-}
{-# LANGUAGE UndecidableInstances #-}

data Foo haveHoge haveHuga
  = Foo
  { hoge :: haveHoge Int
  , huga :: haveHuga Double
  }

deriving instance (Show (a Int), Show (b Double)) => Show (Foo a b)

type InitFoo = Foo Maybe Maybe

noneFoo :: InitFoo
noneFoo = Foo (Just 0) (Just 1.0)

main :: IO ()
main = do
  print noneFoo

うーんボイラープレートが多い。

Bi-functors and nesting

の項があるし対応してるんじゃないですか? いや、ダメですね。今回対応するのはBiとかそう言う次元ではない。レイヤーが少なくとも3つはあるから2つでは対応できない。

しばらくは諦めてボイラープレートを書きます。あまりにも量が多くなるならコード自動生成するコードを書いても良いかもしれません。 GHC.Genericsにあまり詳しくないのでサクッとお出しすることは難しいかもしれませんが…

AesonのFromJSONがフィールドの省略を受け付けない

omitNothingFields の説明によると、

In particular, if the type of a field is declared as a type variable, it will not be omitted from the JSON object, unless the field is specialized upfront in the instance.

とのことなので、型変数としてMaybeが選択される場合、 JSONのフィールドを省略することは出来ないようです。

{"hoge": null}

のようにnullバリューを入れる必要があります。

これが今回の場合だと不便なので、 Aesonの型クラスに対してだけは、結局型シノニムに対してinstance FromJSONすることになりました。

これは完全にボイラープレートになるので、面倒臭さが増えてしまいました。