再帰的な合計タイプを処理するときにコードの重複を減らす方法


50

私は現在、プログラミング言語の簡単なインタープリターに取り組んでおり、次のようなデータ型があります。

data Expr
  = Variable String
  | Number Int
  | Add [Expr]
  | Sub Expr Expr

そして、私は次のような単純なことを行う多くの関数を持っています:

-- Substitute a value for a variable
substituteName :: String -> Int -> Expr -> Expr
substituteName name newValue = go
  where
    go (Variable x)
      | x == name = Number newValue
    go (Add xs) =
      Add $ map go xs
    go (Sub x y) =
      Sub (go x) (go y)
    go other = other

-- Replace subtraction with a constant with addition by a negative number
replaceSubWithAdd :: Expr -> Expr
replaceSubWithAdd = go
  where
    go (Sub x (Number y)) =
      Add [go x, Number (-y)]
    go (Add xs) =
      Add $ map go xs
    go (Sub x y) =
      Sub (go x) (go y)
    go other = other

しかし、これらの各関数では、関数の一部を少し変更するだけで、コードを再帰的に呼び出す部分を繰り返す必要があります。これをより一般的に行うための既存の方法はありますか?この部分をコピーして貼り付ける必要はありません。

    go (Add xs) =
      Add $ map go xs
    go (Sub x y) =
      Sub (go x) (go y)
    go other = other

そして、このようにコードを複製するのは非効率的と思われるため、毎回1つのケースを変更するだけです。

私が思いつくことができる唯一の解決策は、最初にデータ構造全体に対して関数を呼び出し、次に次のように結果に対して再帰的に呼び出す関数を持つことです:

recurseAfter :: (Expr -> Expr) -> Expr -> Expr
recurseAfter f x =
  case f x of
    Add xs ->
      Add $ map (recurseAfter f) xs
    Sub x y ->
      Sub (recurseAfter f x) (recurseAfter f y)
    other -> other

substituteName :: String -> Int -> Expr -> Expr
substituteName name newValue =
  recurseAfter $ \case
    Variable x
      | x == name -> Number newValue
    other -> other

replaceSubWithAdd :: Expr -> Expr
replaceSubWithAdd =
  recurseAfter $ \case
    Sub x (Number y) ->
      Add [x, Number (-y)]
    other -> other

しかし、私はおそらくこれを行うためのもっと簡単な方法があるはずだと感じています。何か不足していますか?


コードの「リフト」バージョンを作成します。何をするかを決定するパラメーター(関数)を使用する場所。次に、リフトされたバージョンに関数を渡すことにより、特定の関数を作成できます。
Willem Van Onsem

あなたの言語は簡略化できると思います。のAdd :: Expr -> Expr -> Expr代わりに定義しAdd :: [Expr] -> ExprSub完全に取り除きます。
chepner

この定義を単純化したバージョンとして使用しています。この場合は問題ありませんが、言語の他の部分の式のリストも含めることができる必要があります
Scott

といった?すべてではないにしても、ほとんどの連鎖演算子はネストされた2項演算子に削減できます。
chepner

1
私はあなたrecurseAfterana変装していると思います。アナモルフィズムとを見てみたいかもしれませんrecursion-schemes。そうは言っても、あなたの最終的な解決策は可能な限り短いと思います。公式のrecursion-schemesアナモルフィズムに切り替えても、それほどの節約にはなりません。
チー

回答:


38

おめでとうございます、アナモルフィズムを再発見しました!

これがrecursion-schemesパッケージで動作するように言い換えたコードです。残念ながら、機械を動作させるには定型文が必要なので、短くはありません。(定型文を使用するなど、ボイラープレートを回避するいくつかの自動魔法の方法があるかもしれません。私は単に知りません。)

以下では、recurseAfterが標準に置き換えられますana

まず、再帰型と、それが固定小数点であるファンクターを定義します。

{-# LANGUAGE DeriveFunctor, TypeFamilies, LambdaCase #-}
{-# OPTIONS -Wall #-}
module AnaExpr where

import Data.Functor.Foldable

data Expr
  = Variable String
  | Number Int
  | Add [Expr]
  | Sub Expr Expr
  deriving (Show)

data ExprF a
  = VariableF String
  | NumberF Int
  | AddF [a]
  | SubF a a
  deriving (Functor)

次に、2つをいくつかのインスタンスに接続しExprて、同型ExprF Exprに展開し、折り返すことができるようにします。

type instance Base Expr = ExprF
instance Recursive Expr where
   project (Variable s) = VariableF s
   project (Number i) = NumberF i
   project (Add es) = AddF es
   project (Sub e1 e2) = SubF e1 e2
instance Corecursive Expr where
   embed (VariableF s) = Variable s
   embed (NumberF i) = Number i
   embed (AddF es) = Add es
   embed (SubF e1 e2) = Sub e1 e2

最後に、元のコードを適応させ、いくつかのテストを追加します。

substituteName :: String -> Int -> Expr -> Expr
substituteName name newValue = ana $ \case
    Variable x | x == name -> NumberF newValue
    other                  -> project other

testSub :: Expr
testSub = substituteName "x" 42 (Add [Add [Variable "x"], Number 0])

replaceSubWithAdd :: Expr -> Expr
replaceSubWithAdd = ana $ \case
    Sub x (Number y) -> AddF [x, Number (-y)]
    other            -> project other

testReplace :: Expr
testReplace = replaceSubWithAdd 
   (Add [Sub (Add [Variable "x", Sub (Variable "y") (Number 34)]) (Number 10), Number 4])

別の方法として、 ExprF aのみを行ってから導出type Expr = Fix ExprF。これにより、上記のボイラープレートの一部(2つのインスタンスなど)が節約されますが、他のコンストラクタと同様に、のFix (VariableF ...)代わりに使用する必要がありますVariable ...

さらに、パターンの同義語を使用することでそれを軽減することができます(ただし、定型文は少し増えます)。


更新:私はついにテンプレートHaskellを使用してautomagicツールを見つけました。これにより、コード全体がかなり短くなります。注ことExprFファンクタ、まだ上記の2つのインスタンスは、ボンネットの下に存在し、我々はまだそれらを使用する必要があります。手動で定義する手間を省くだけですが、それだけで多くの労力を節約できます。

{-# LANGUAGE DeriveFunctor, DeriveTraversable, TypeFamilies, LambdaCase, TemplateHaskell #-}
{-# OPTIONS -Wall #-}
module AnaExpr where

import Data.Functor.Foldable
import Data.Functor.Foldable.TH

data Expr
  = Variable String
  | Number Int
  | Add [Expr]
  | Sub Expr Expr
  deriving (Show)

makeBaseFunctor ''Expr

substituteName :: String -> Int -> Expr -> Expr
substituteName name newValue = ana $ \case
    Variable x | x == name -> NumberF newValue
    other                  -> project other

testSub :: Expr
testSub = substituteName "x" 42 (Add [Add [Variable "x"], Number 0])

replaceSubWithAdd :: Expr -> Expr
replaceSubWithAdd = ana $ \case
    Sub x (Number y) -> AddF [x, Number (-y)]
    other            -> project other

testReplace :: Expr
testReplace = replaceSubWithAdd 
   (Add [Sub (Add [Variable "x", Sub (Variable "y") (Number 34)]) (Number 10), Number 4])

あなたは本当にではExprなく、明示的に定義する必要がありますtype Expr = Fix ExprFか?
chepner

2
@chepner代替案として簡単に触れました。すべてにダブルコンストラクタを使用する必要があるのは少し不便Fixです。TH自動化で最後のアプローチを使用する方がいいです、IMO。
カイ

19

代替アプローチとして、これはuniplateパッケージの一般的な使用例でもあります。Data.DataテンプレートHaskellではなくジェネリックを使用してボイラープレートを生成することができるため、Dataインスタンスを派生させる場合Expr

import Data.Data

data Expr
  = Variable String
  | Number Int
  | Add [Expr]
  | Sub Expr Expr
  deriving (Show, Data)

次に、transformfrom関数はData.Generics.Uniplate.Dataネストされた各関数に再帰的に関数を適用しますExpr

import Data.Generics.Uniplate.Data

substituteName :: String -> Int -> Expr -> Expr
substituteName name newValue = transform f
  where f (Variable x) | x == name = Number newValue
        f other = other

replaceSubWithAdd :: Expr -> Expr
replaceSubWithAdd = transform f
  where f (Sub x (Number y)) = Add [x, Number (-y)]
        f other = other

replaceSubWithAdd特に、この関数fは非再帰的な置換を実行するように記述されていることに注意してください。transformで再帰的にx :: Exprなるためana、@ chiの回答と同じようにヘルパー関数に同じ魔法をかけています。

> substituteName "x" 42 (Add [Add [Variable "x"], Number 0])
Add [Add [Number 42],Number 0]
> replaceSubWithAdd (Add [Sub (Add [Variable "x", 
                     Sub (Variable "y") (Number 34)]) (Number 10), Number 4])
Add [Add [Add [Variable "x",Add [Variable "y",Number (-34)]],Number (-10)],Number 4]
> 

これは、@ chiのテンプレートHaskellソリューションよりも短くはありません。1つの潜在的な利点は、uniplate役立つかもしれないいくつかの追加機能を提供することです。たとえばdescend、の代わりにを使用した場合transform、それは直接の子れ、再帰が発生する場所を制御できます。または、を使用rewriteして、固定点に到達するまで変換の結果を再変換できます。潜在的な欠点の1つは、「アナモルフィズム」が「ユニプレート」よりも格好よく聞こえることです。

完全なプログラム:

{-# LANGUAGE DeriveDataTypeable #-}

import Data.Data                     -- in base
import Data.Generics.Uniplate.Data   -- package uniplate

data Expr
  = Variable String
  | Number Int
  | Add [Expr]
  | Sub Expr Expr
  deriving (Show, Data)

substituteName :: String -> Int -> Expr -> Expr
substituteName name newValue = transform f
  where f (Variable x) | x == name = Number newValue
        f other = other

replaceSubWithAdd :: Expr -> Expr
replaceSubWithAdd = transform f
  where f (Sub x (Number y)) = Add [x, Number (-y)]
        f other = other

replaceSubWithAdd1 :: Expr -> Expr
replaceSubWithAdd1 = descend f
  where f (Sub x (Number y)) = Add [x, Number (-y)]
        f other = other

main = do
  print $ substituteName "x" 42 (Add [Add [Variable "x"], Number 0])
  print $ replaceSubWithAdd e
  print $ replaceSubWithAdd1 e
  where e = Add [Sub (Add [Variable "x", Sub (Variable "y") (Number 34)])
                     (Number 10), Number 4]
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.