web-dev-qa-db-ja.com

ネストされたデータ構造を更新するためのHaskellイディオムはありますか?

野球選手、チーム、コーチの統計を追跡するために、次のデータモデルがあるとしましょう。

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のレコード構文によって提供されるすべてのアクセサーを使用しますが、醜く反復的であり、保守と読み取りが難しいと思います。

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-関数型プログラミングとHaskellおよび静的のコンテキストでupdate-inを理解しようとしていると思いますタイピング。

43
Matt Fenwick

レコード更新構文は、コンパイラに標準で付属しています。

addManStk team = team {
    manager = (manager team) {
        diet = (diet (manager team)) {
             steaks = steaks (diet (manager team)) + 1
             }
        }
    }

ひどい!しかし、もっと良い方法があります。 Hackageには、機能リファレンスとレンズを実装するパッケージがいくつかあります。これは間違いなくあなたがやりたいことです。たとえば、 fclabels パッケージでは、すべてのレコード名の前にアンダースコアを付けてから、次のように記述します。

$(mkLabels ['BBTeam, 'Coach, 'Diet, 'BBPlayer])
addManStk = modify (+1) (steaks . diet . manager)

2017年に編集して追加:最近では、 lens パッケージが特に優れた実装手法であるという幅広いコンセンサスがあります。これは非常に大きなパッケージですが、Webのさまざまな場所で入手できる非常に優れたドキュメントと紹介資料もあります。

38
Daniel Wagner

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

次に、SECを作成して、必要なことを言います。つまり、(チームの)マネージャーの食事の賭け金に1を追加します。

addManagerSteak :: Unop BBTeam
addManagerSteak = (onManager . onDiet . onStakes) (+1)

SYBアプローチと比較すると、SECバージョンでは、SECを定義するために追加の作業が必要であり、この例で必要なものだけを提供しました。 SECは、対象を絞ったアプリケーションを許可しています。これは、プレーヤーがダイエットをしていても、微調整したくない場合に役立ちます。おそらく、その区別を処理するためのかなりSYBの方法もあります。

編集:基本的なSECの代替スタイルは次のとおりです。

onManager :: Lifter Coach BBTeam
onManager f t = t { manager = f (manager t) }
11
Conal

後で、いくつかのジェネリックプログラミングライブラリも確認することをお勧めします。データの複雑さが増し、ボイラープレートコード(プレーヤーのステーキコンテンツ、コーチの食事、ウォッチャーのビールコンテンツの増加など)を記述していることに気付いた場合。あまり冗長でない形式でもまだ定型です。 [〜#〜] syb [〜#〜] はおそらく最もよく知られているライブラリです(そしてHaskellプラットフォームに付属しています)。実際、 SYBに関する元の論文 は、非常によく似た問題を使用して、アプローチを示しています。

会社の組織構造を説明する次のデータ型について考えてみます。会社は部門に分かれています。各部門にはマネージャーがいて、サブユニットのコレクションで構成されています。ユニットは、単一の従業員または部門のいずれかです。マネージャーも一般社員も給料をもらっているだけです。

[スキップ]

ここで、会社の全員の給与を指定された割合で増やしたいとします。つまり、次の関数を作成する必要があります。

増加::フロート->会社->会社

(残りは紙にあります-読むことをお勧めします)

もちろん、あなたの例では、小さなデータ構造の1つにアクセス/変更するだけでよいので、一般的なアプローチは必要ありません(それでも、タスクのSYBベースのソリューションは以下にあります)が、コード/アクセスのパターン/アクセスの繰り返しが表示されたらこれをチェックしたい変更または other ジェネリックプログラミングライブラリ。

{-# 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)
5
Ed'ka