วิธีลดการทำสำเนารหัสเมื่อจัดการกับประเภทผลรวมซ้ำ


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

ฉันแค่ใช้คำจำกัดความนี้เป็นเวอร์ชันที่ง่ายขึ้น ในขณะที่จะทำงานในกรณีนี้ผมจะต้องสามารถที่จะมีรายชื่อของการแสดงออกสำหรับส่วนอื่น ๆ ของภาษาได้เป็นอย่างดี
สกอตต์

เช่น? ส่วนใหญ่หากไม่ใช่ทั้งหมดตัวดำเนินการที่ถูกห่วงโซ่สามารถลดลงเป็นตัวดำเนินการไบนารีที่ซ้อนกัน
chepner

1
ผมคิดว่าคุณrecurseAfterมีanaในการปลอมตัว คุณอาจต้องการที่จะดูที่ anamorphisms recursion-schemesและ ที่ถูกกล่าวว่าฉันคิดว่าทางออกสุดท้ายของคุณสั้นที่สุดเท่าที่จะทำได้ การเปลี่ยนไปใช้แอนrecursion-schemesมอร์ฟิซึมอย่างเป็นทางการจะไม่ประหยัดมากนัก
Chi

คำตอบ:


38

ขอแสดงความยินดีคุณเพิ่งค้นพบสิ่งมีชีวิตอีกครั้ง!

นี่คือรหัสของคุณ rephrased เพื่อให้ทำงานกับrecursion-schemesแพคเกจ อนิจจามันไม่ได้สั้นลงเนื่องจากเราต้องใช้แผ่นบางแผ่นเพื่อให้เครื่องจักรทำงานได้ (อาจมีวิธีอัตโนมัติเพื่อหลีกเลี่ยงการสร้างเช่นการใช้ยาทั่วไปฉันไม่ทราบ)

ด้านล่างนี้คุณจะถูกแทนที่ด้วยมาตรฐานrecurseAfterana

เรากำหนดประเภทเวียนเกิดของคุณก่อนเช่นเดียวกับ 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วิธีนี้จะช่วยประหยัดบางส่วนของ boilerplate ด้านบน (เช่นสองกรณี) ที่ค่าใช้จ่ายของการใช้Fix (VariableF ...)แทนVariable ...เช่นเดียวกับที่คล้ายกันสำหรับการก่อสร้างอื่น ๆ

เราสามารถลดการใช้คำพ้องความหมายของรูปแบบได้อีก (โดยเสียค่าใช้จ่ายเพิ่มมากขึ้นเล็กน้อย)


อัปเดต: ในที่สุดฉันก็พบเครื่องมืออัตโนมัติโดยใช้เทมเพลต Haskell ทำให้รหัสทั้งหมดสั้นพอสมควร โปรดทราบว่าExprFfunctor และทั้งสองกรณีข้างต้นยังคงอยู่ภายใต้ประทุนและเรายังต้องใช้มัน เราบันทึกความยุ่งยากในการกำหนดด้วยตนเองเท่านั้น แต่การบันทึกเพียงอย่างเดียวนั้นช่วยประหยัดได้มาก

{-# 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 automation คือ nicer, IMO
Chi

19

เป็นวิธีทางเลือกนี่เป็นกรณีการใช้งานทั่วไปสำหรับuniplateแพ็คเกจ มันสามารถใช้Data.Dataยาชื่อสามัญมากกว่า Template Haskell เพื่อสร้างแผ่นสำเร็จรูปดังนั้นหากคุณได้รับDataอินสแตนซ์สำหรับExpr:

import Data.Data

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

จากนั้นtransformฟังก์ชัน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]
> 

นี่ไม่สั้นกว่าโซลูชันเทมเพลต Haskell ของ @ chi ข้อดีอย่างหนึ่งที่เป็นไปได้คือuniplateให้ฟังก์ชั่นเพิ่มเติมบางอย่างที่อาจเป็นประโยชน์ ตัวอย่างเช่นถ้าคุณใช้descendแทนที่transformมันจะเปลี่ยนเฉพาะเด็ก ๆ ในทันทีที่สามารถให้คุณควบคุมว่าเกิดการเรียกซ้ำเกิดขึ้นอีกหรือคุณสามารถใช้rewriteเพื่อแปลงผลการแปลงอีกครั้งจนกว่าจะถึงจุดคงที่ ข้อเสียอย่างหนึ่งที่อาจเกิดขึ้นคือ "Anamorphism" ฟังดูดีกว่า "uniplate"

โปรแกรมเต็มรูปแบบ:

{-# 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.