재귀 합계 유형을 처리 할 때 코드 중복을 줄이는 방법


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

이와 같은 코드를 복제하는 것은 비효율적이므로 매번 단일 사례를 변경하십시오.

내가 취할 수있는 유일한 해결책은 전체 데이터 구조에서 먼저 함수를 호출 한 다음 다음과 같이 결과를 재귀 적으로 호출하는 함수를 갖는 것입니다.

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] -> Expr하고 Sub완전히 제거 하십시오 .
chepner

이 정의를 단순화 된 버전으로 사용하고 있습니다. 이 경우에는 효과가 있지만, 언어의 다른 부분에 대한 표현 목록도 포함 할 수 있어야합니다.
Scott

같은? 전부는 아니지만 대부분의 연쇄 연산자를 중첩 이진 연산자로 줄일 수 있습니다.
chepner

1
나는 당신의 생각 recurseAfterIS를 ana변장. 아나 몰 피즘을보고 싶을 수도 있습니다 recursion-schemes. 즉, 최종 솔루션이 가능한 한 짧다고 생각합니다. 공식 recursion-schemes아나 모피 즘으로 전환 해도 많은 비용이 절약되지는 않습니다.
chi

답변:


38

축하합니다. 아나 모르 피즘을 재발견하셨습니다!

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])

실제로 Expr같은 것이 아니라 명시 적 으로 정의해야 type Expr = Fix ExprF합니까?
chepner

2
@ chepner 나는 대안으로 간단히 언급했습니다. 모든 것에 대해 이중 생성자를 사용해야하는 것은 약간 불편합니다. Fix+ 실제 생성자. TH 자동화와 함께 마지막 접근 방식을 사용하는 것이 더 좋습니다.
chi

19

다른 방법으로, 이는 uniplate패키지 의 일반적인 사용 사례이기도합니다 . Data.DataTemplate Haskell 대신 generics 를 사용 하여 상용구를 생성 할 수 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에서 재귀 적이므로 @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]
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.