ネストされたデータ構造を更新するためのHaskell Idiomはありますか?
-
28-10-2019 - |
質問
野球選手、チーム、コーチの統計を追跡するために、次のデータモデルがあるとしましょう。
data BBTeam = BBTeam { teamname :: String,
manager :: Coach,
players :: [BBPlayer] }
deriving (Show)
data Coach = Coach { coachname :: String,
favcussword :: String,
diet :: Diet }
deriving (Show)
data Diet = Diet { dietname :: String,
steaks :: Integer,
eggs :: Integer }
deriving (Show)
data BBPlayer = BBPlayer { playername :: String,
hits :: Integer,
era :: Double }
deriving (Show)
ここで、通常はステーキの狂信者であるマネージャーがさらにステーキを食べたいと思っているとしましょう。したがって、マネージャーの食事のステーキ含有量を増やすことができる必要があります。この関数の2つの可能な実装を次に示します。
1)これは多くのパターンマッチングを使用しており、すべてのコンストラクターのすべての引数順序を正しく取得する必要があります... 2回。それはあまり上手くスケーリングされないか、非常に保守可能/読み取り可能ではないようです。
addManagerSteak :: BBTeam -> BBTeam
addManagerSteak (BBTeam tname (Coach cname cuss (Diet dname oldsteaks oldeggs)) players) = BBTeam tname newcoach players
where
newcoach = Coach cname cuss (Diet dname (oldsteaks + 1) oldeggs)
2)これは、HaskellのRecord Syntaxによって提供されるすべてのアクセターを使用しますが、醜くて繰り返し、維持して読むのが難しいと思います。
addManStk :: BBTeam -> BBTeam
addManStk team = newteam
where
newteam = BBTeam (teamname team) newmanager (players team)
newmanager = Coach (coachname oldcoach) (favcussword oldcoach) newdiet
oldcoach = manager team
newdiet = Diet (dietname olddiet) (oldsteaks + 1) (eggs olddiet)
olddiet = diet oldcoach
oldsteaks = steaks olddiet
私の質問は、これらの1つが他のものよりも優れているのか、それともHaskellコミュニティ内でより好まれているのか?これを行うためのより良い方法はありますか(コンテキストを維持しながらデータ構造の奥深くに値を変更する)?私は効率性を心配していません。単に優雅さ/一般性/保守性をコードします。
Clojureには、この問題(または同様の問題)のために何かがあることに気付きました。 update-in
- だから私は理解しようとしていると思います update-in
機能的なプログラミングとHaskellおよび静的タイピングのコンテキストで。
解決
レコード更新構文は、コンパイラに標準装備されています。
addManStk team = team {
manager = (manager team) {
diet = (diet (manager team)) {
steaks = steaks (diet (manager team)) + 1
}
}
}
ひどい!しかし、より良い方法があります。ハッキングには、機能的な参照とレンズを実装するいくつかのパッケージがあります。これは間違いなくやりたいことです。たとえば、 fclabels パッケージ、あなたはすべてのレコード名の前にアンダースコアを置き、それから書きます
$(mkLabels ['BBTeam, 'Coach, 'Diet, 'BBPlayer])
addManStk = modify (+1) (steaks . diet . manager)
追加するために2017年に編集:最近では、 レンズ パッケージは特に優れた実装手法です。非常に大きなパッケージですが、ウェブ上のさまざまな場所で利用できる非常に優れたドキュメントと入門資料もあります。
他のヒント
Lambdageekが示唆したように、セマンティックエディターの組み合わせ(SEC)を使用する方法は次のとおりです。
最初にいくつかの役立つ略語:
type Unop a = a -> a
type Lifter p q = Unop p -> Unop q
Unop
これが「セマンティックエディター」と Lifter
セマンティックエディターの組み合わせです。いくつかのリフター:
onManager :: Lifter Coach BBTeam
onManager f (BBTeam n m p) = BBTeam n (f m) p
onDiet :: Lifter Diet Coach
onDiet f (Coach n c d) = Coach n c (f d)
onStakes :: Lifter Integer Diet
onStakes f (Diet n s e) = Diet n (f s) e
今、あなたが望むものを言うために秒を構成するだけです。つまり、(チームの)マネージャーの食事の賭けに1を追加します。
addManagerSteak :: Unop BBTeam
addManagerSteak = (onManager . onDiet . onStakes) (+1)
SYBアプローチと比較すると、SECバージョンではSECを定義するために追加の作業が必要であり、この例で必要なものを提供するだけです。 SECはターゲットを絞ったアプリケーションを許可します。これは、プレイヤーが食事をしている場合に役立ちますが、それらを微調整したくありませんでした。おそらく、その区別を処理するためのかなりのサイブの方法があるでしょう。
編集: 基本的なSECの代替スタイルは次のとおりです。
onManager :: Lifter Coach BBTeam
onManager f t = t { manager = f (manager t) }
後であなたはいくつかの一般的なプログラミングライブラリを見てみたいと思うかもしれません:あなたのデータの複雑さが増加し、あなたがより多くを書いていることに気付いたとき、そしてボイラープレートコード(プレイヤーのためのステーキコンテンツの増加、コーチのダイエット、ウォッチャーのビールコンテンツなど)冗長な形でもまだボイラープレートです。syb おそらく最もよく知られているライブラリです(そして、Haskellプラットフォームが付属しています)。実際、 Sybに関するオリジナルペーパー 非常によく似た問題を使用して、アプローチを実証します。
会社の組織構造を説明する次のデータ型を考えてください。会社は部門に分かれています。各部門にはマネージャーがあり、ユニットが単一の従業員または部門のいずれかであるサブユニットのコレクションで構成されています。マネージャーと普通の従業員の両方は、給与を受け取る人です。
スキップ
ここで、会社の全員の給与を指定された割合で増やしたいとします。つまり、関数を書く必要があります。
増加:: float-> company-> company
(残りは論文にあります - 読書をお勧めします)
もちろん、例では、小さなデータ構造の1つの部分にアクセス/変更するだけで、一般的なアプローチが必要ありません(タスクのSYBベースのソリューションは以下にあります)が、アクセスのコード/パターンを繰り返していると確認したら/修正あなたは私がこれをチェックしたいです 他の 一般的なプログラミングライブラリ。
{-# LANGUAGE DeriveDataTypeable #-}
import Data.Generics
data BBTeam = BBTeam { teamname :: String,
manager :: Coach,
players :: [BBPlayer]} deriving (Show, Data, Typeable)
data Coach = Coach { coachname :: String,
favcussword :: String,
diet :: Diet } deriving (Show, Data, Typeable)
data Diet = Diet { dietname :: String,
steaks :: Integer,
eggs :: Integer} deriving (Show, Data, Typeable)
data BBPlayer = BBPlayer { playername :: String,
hits :: Integer,
era :: Double } deriving (Show, Data, Typeable)
incS d@(Diet _ s _) = d { steaks = s+1 }
addManagerSteak :: BBTeam -> BBTeam
addManagerSteak = everywhere (mkT incS)