현재 프로그래밍 언어에 대한 간단한 인터프리터로 작업하고 있으며 다음과 같은 데이터 유형이 있습니다.
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
이와 같은 코드를 복제하는 것은 비효율적이므로 매번 단일 사례를 변경하십시오.
내가 취할 수있는 유일한 해결책은 전체 데이터 구조에서 먼저 함수를 호출 한 다음 다음과 같이 결과를 재귀 적으로 호출하는 함수를 갖는 것입니다.
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
그러나 이미이 작업을 수행하는 더 간단한 방법이 있어야한다고 생각합니다. 뭔가 빠졌습니까?
답변
축하합니다. 아나 모르 피즘을 재발견하셨습니다!
recursion-schemes
패키지 와 함께 작동하도록 코드를 정리했습니다 . 아아, 기계가 작동하려면 보일러 플레이트가 필요하기 때문에 짧지 않습니다. (예를 들어, generics를 사용하는 등 상용구를 피할 수있는 몇 가지 자동 방법이있을 수 있습니다. 단순히 모르겠습니다.)
아래에서 귀하 recurseAfter
의 표준으로 대체되었습니다 ana
.
우리는 먼저 재귀 유형과 고정 점의 functor를 정의합니다.
{-# 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)
그런 다음 두 인스턴스를 몇 개의 인스턴스 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
. 이것은 사용하지의 비용으로, 위의 상용구 (예를 들어, 두 개의 인스턴스)의 일부를 저장 Fix (VariableF ...)
하는 대신 Variable ...
,뿐만 아니라 다른 생성자에 대한 유사.
패턴 동의어를 사용하는 것 (하지만 조금 더 보일러 플레이트를 사용하는 비용)을 더 완화시킬 수 있습니다.
업데이트 : 마침내 Haskell 템플릿을 사용하여 자동 도구를 찾았습니다. 이것은 전체 코드를 합리적으로 짧게 만듭니다. 참고 것을 ExprF
펑 여전히 위의 두 경우는 후드 아래에 존재하며, 우리는 여전히 그들을 사용해야합니다. 우리는 그것들을 수동으로 정의해야하는 번거 로움을 덜어 주지만 이것만으로도 많은 노력을 절약 할 수 있습니다.
{-# 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])
답변
다른 방법으로, 이는 uniplate
패키지 의 일반적인 사용 사례이기도합니다 . Data.Data
Template Haskell 대신 generics 를 사용 하여 상용구를 생성 할 수 Data
있습니다 Expr
.
import Data.Data
data Expr
= Variable String
| Number Int
| Add [Expr]
| Sub Expr Expr
deriving (Show, Data)
그런 다음 transform
from 함수는 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
에서 재귀 적이므로 @chi의 답변에서 x :: Expr
와 같이 도우미 함수에 동일한 마법을 수행합니다 ana
.
> 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의 Template Haskell 솔루션보다 짧지 않습니다. 잠재적 이점 중 하나는 uniplate
도움이 될 수있는 몇 가지 추가 기능을 제공한다는 것입니다. 예를 들어 descend
대신 대신 사용 하면 재귀가 발생하는 위치를 제어 할 수 transform
있는 직계 자식 만 변환 하거나 rewrite
고정 소수점에 도달 할 때까지 변환 결과를 다시 변환하는 데 사용할 수 있습니다 . 한 가지 잠재적 인 단점은 “아나 모르 피즘”이 “유니 플레이트”보다 더 시원하게 들린다는 것입니다.
전체 프로그램 :
{-# 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]