最近見かけたHaskell library: cache

cacheはシンプルなin-memory key-value store. 内部実装としてHashMapを使っており、IOやSTMモナドによるinsert, lookupが可能。また、オプションでexpiration timeをセットすることもできる。

Redisやmemcachedなどを使うまでもない場合や、テストコードなんかで重宝しそう。

GHC.Generics.Generic typeclassと実装隠蔽とGHC.Records.HasField typeclass

ここ最近、ちょっとHaskellを書く機会があり、Haskellにおける伝統的な課題「レコードフィールド名が広めの名前空間に飛び出るので衝突しまくる問題」をなんとかできないか考えている。

DuplicateRecordFields拡張で、一応レコードフィールド名の(他のレコードのフィールド名や関数名との)衝突があってもコンパイルは通るようになった。しかし、selector functionが使いづらい状況には変わりないので、レコードフィールドの値を参照する場合にかなり面倒になる。今のところ、レコードのpattern matchをしてNamedFieldPuns拡張RecordWildCards拡張を使う、というやり方をしているが、これも一つのレコードオブジェクトのフィールド名が対象の名前空間にそのまんま飛び出てくるという点がいけすかない。複数のレコードオブジェクトを扱う場合は結局名前の衝突が起きがちだ。

GHC 9.2ではOverloadedRecordDot拡張があるので、この問題のかなりの部分が解決するのではないだろうか。

ただ、OverloadedRecordUpdate拡張は実装上、未完成かつ実験的位置づけらしい。個人的にはgetterだけでもあるととても助かるので、setterが後回しになってもまあ、構わない。

とはいえ、まだまだGHC-8系を使う機会が多いので、まだOverloadedRecordDotを使いまくるわけにはいかなさそう。

ということで、generic-lensパッケージを試してみた。このパッケージはGHC.GenericsモジュールのGeneric instanceを持つデータ型に対して、フィールドアクセサに相当するlensを自動導出するというもの。

リファレンスマニュアルが難解な割に公式でまともなチュートリアルがないのがつらい。。 使い方については以下の記事が一番分かりやすいと思う。

とても便利なものだと思ったが、ここで一つ気になることがあった。generic-lensでは、privateなデータ型についてもフィールドアクセサを生成して、内部構造をバンバン更新できるのだ。ここでいうprivateなデータ型というのは、平たくいえばdata constructorをexportしていないデータ型、ということである。

詳しくは下記issueに書いた。

例えば以下のmodule Aがある。Private型はdata constructorをexportしていない、privateなデータ型である。

module A (Private, newPrivate) where

import GHC.Generics (Generic)

-- | The data constructor is not exposed.
data Private = Private { foo :: Int } deriving (Show,Generic)

-- | A smart constructor
newPrivate :: Int -> Private
newPrivate = Private

で、これを扱うmodule Mainを定義する。

module Main (main) where

import A (Private, newPrivate)
import Data.Generics.Product (HasField(..))
import Lens.Micro.Platform ((.~))

main :: IO ()
main = putStrLn $ show $ (field @"foo" .~ 10) $ newPrivate 5

module Mainではgeneric-lensを使ってPrivate型の中身をゴリゴリに書き換えている。このコードはコンパイルが通る。これはPrivate型の実装を隠蔽したいという設計を突き破っているのではないだろうか?

上記issueで質問を投げかけたところ、直ちにハッとさせられる返答が返ってきた。Private型についてGeneric instanceをderiveした時点で、その型の内部実装をさらけ出しているのだ、と。

なるほど、言われてみればそうだ。Generic instanceは、そのデータ型の内部構造をプログラム内で値として扱えるようにするもの。いくらdata constructorだけ隠蔽したとしても、よそのコードはGeneric instanceを通じてそのデータ型の内部構造にアクセスできるわけだ。

そう考えると、実装隠蔽したいデータ型についてはGeneric instanceをderiveしないほうがいい気がしてくる。今までは、なんなら「何かと便利だし、全部のデータ型についてGenericをderiveしておけばよくね」くらいに考えていたが、そうでもなさそうだ。

とはいえ、自分がderive Genericしたくなるユースケースは今のところToJSON, FromJSONのderiveのためくらいだろうか。こういったserialization用のデータ型はそもそも実装隠蔽する意味があまりないので、derive Genericしていいだろう。

なお、「データ型が定義されたモジュール内ではderive Genericして、モジュール外には実装隠蔽したい」というケースでは、export用にnewtype wrapperをかませる手がありそう。

module A (Exported) where

import GHC.Generics (Generic)

data Internal = Internal { foo :: Int } deriving (Show,Generic)

newtype Exported = Exported Internal

ところで、OverloadedRecordDot拡張はGHC.RecordsモジュールのHasField typeclassに関する構文糖衣として実装されるらしい。実はGHC.RecordsとHasField typeclass自体はGHC-8系にも既に実装されていて、使うことができる。

例えば、ghc-8でも以下のような書き方が可能なようだ。

module A (Public (..), Private, add) where

import GHC.Records (HasField (..))

data Public = Public { foo :: Int } deriving (Show)

data Private = Private { foo :: Int } deriving (Show)

add :: Public -> Private -> Int
add pub pri = getField @"foo" pub + getField @"foo" pri

GHC manualによれば、GHC.RecordsのHasField typeclass constraintは、当該フィールド名が当該名前空間(モジュール)にimportされている場合にsolveされるらしい。

つまり、上記のmodule Aを使う場合、

module Main (main) where

import GHC.Records (HasField (..))
import A           (Private, Public (..))

main :: IO ()
main = putStrLn $ show $ add' (Public 10) 20

-- add :: Public -> Private -> Int
-- add pub pri = getField @"foo" pub + getField @"foo" pri

add' :: Public -> Int -> Int
add' pub n = getField @"foo" pub + n

module Main内のadd関数のコメントアウトを外すと、Privatefoo fieldがimportされていないので以下のようなコンパイルエラーが出る。

• No instance for (HasField "foo" Private Int)
    arising from a use of ‘getField’

つまり、GHC.Records.HasField typeclassは実装隠蔽の設計を正しく反映してくれそうだ。

OverloadedRecordDot拡張がない場合、GHC.Records.HasFieldtypeclassを使うにはgetField関数を明示的に書く必要はあるが、それでも十分すぎるくらい便利なのでは。

最近見かけたHaskell library: acc

accはリストデータ構造Accを提供するライブラリ。Accの特徴はappendやprependが速い、ということ。ベンチマークによるとDListSeqより速いとのこと。とにかくデータをリスト状に突っ込むようなユースケースに使える。

内部的にはbinary treeで実装されているようだ。

最近見かけたHaskell library: valida

validaはproduct type (レコードデータ型)に対するvalidation処理を書くためのライブラリ。validation処理はValidator型で表現される。

data Validator e inp a
  • e: エラーを示す型。基本的にSemigroupであることを想定。発生したエラーはeの(<>)演算子で結合、集積される。
  • inp: validationの対象となる入力型。
  • a: validation済みのデータを示す出力型。

ValidatorSemigroup e => Applicative (Validator e inp)というインスタンスを持っている。これにより、applicative styleで出力型aを組み立てることでValidatorを作ることができる。

なお、Validatorを実行するにはrunValidatorを使う、とドキュメントに書いてあるが、runValidator関数がドキュメントに見当たらない。ソースを読むと、runValidatorValidatorのdata constructorらしい。型は、

runValidator :: Validator e inp a -> inp -> Validation e a

とのこと。

最近見かけたHaskell library: slugger

sluggerは任意の文字列をURL-friendlyな文字列にそれっぽく変換するライブラリ。例えば

import qualified Data.String.Slugger as SluggerString

SluggerString.toSlug "Hey there,   world!"
-- "hey-there-world"

SluggerString.toSlug "GARÇON - déjà , Forêt — Zoë"
-- "garcon-deja-foret-zoe"

このような変換をする。例えばブログ記事のパーマリンクを自動生成するのに役に立ちそう。

残念ながら日本語には非対応 (そもそもどうやって対応させるのが妥当なのか分からないが)

最近見かけたHaskell library: retry

retryは任意のIOアクションをバックオフつきで再試行(リトライ)する仕組みを提供するライブラリ。RetryPolicy型(Monoidになっている)でどういった戦略でリトライをするかを設定し、retrying関数を使ってRetryPolicyに従ったリトライつきIOアクションを実行する。

ネットワークIOではとかくリトライをどうやるかが問題になるが、本ライブラリを使うことでリトライのロジックと本来やりたい仕事の関心の分離をきれいに実現できる。