पुनरावर्ती योग प्रकारों से निपटने पर कोड दोहराव कैसे कम करें


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

लेकिन मुझे लगता है कि पहले से ही ऐसा करने का एक सरल तरीका होना चाहिए। क्या मैं कुछ भूल रहा हूँ?


कोड का "उठा हुआ" संस्करण बनाएं। जहां आप मापदंडों (कार्यों) का उपयोग करते हैं जो तय करते हैं कि क्या करना है। फिर आप उठाए गए संस्करण में फ़ंक्शन पास करके विशिष्ट कार्य कर सकते हैं।
विलेम वैन ओन्सेम

मुझे लगता है कि आपकी भाषा सरल हो सकती है। Add :: Expr -> Expr -> Exprइसके बजाय परिभाषित करें Add :: [Expr] -> Expr, और Subपूरी तरह से छुटकारा पाएं ।
चेपनर

मैं इस परिभाषा को एक सरलीकृत संस्करण के रूप में उपयोग कर रहा हूं; जबकि वह इस मामले में काम करेगा, मुझे भाषा के अन्य भागों के लिए भावों की सूची समाहित करने में सक्षम होना चाहिए
स्कॉट

जैसे कि? अधिकांश, यदि सभी नहीं, तो जंजीर ऑपरेटरों को नेस्टेड बाइनरी ऑपरेटरों को कम किया जा सकता है।
चेपनर

1
मुझे लगता है कि आपके recurseAfterहै anaभेस में। आप एनामर्फिम्स को देखना चाहते हैं और कर सकते हैं 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)

फिर हम दोनों को कुछ उदाहरणों से जोड़ते हैं ताकि हम 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 ..., साथ ही साथ अन्य कंस्ट्रक्टरों के लिए अनुरूप।

आगे यह कहा जा सकता है कि पैटर्न समानार्थक शब्द का उपयोग करना (थोड़ा अधिक बॉयलरप्लेट की कीमत पर, हालांकि)।


अपडेट: मैंने अंत में टेम्पलेट हास्केल का उपयोग करके, स्वचालित उपकरण पाया। यह पूरे कोड को यथोचित रूप से छोटा बनाता है। ध्यान दें कि 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?
चेपनर

2
@chepner मैंने संक्षेप में एक विकल्प के रूप में उल्लेख किया है। हर चीज के लिए डबल कंस्ट्रक्टर का उपयोग करना थोड़ा असुविधाजनक है: Fix+ वास्तविक कंस्ट्रक्टर। TH स्वचालन के साथ अंतिम दृष्टिकोण का उपयोग करना अच्छा है, IMO।
ची

19

वैकल्पिक दृष्टिकोण के रूप में, यह uniplateपैकेज के लिए एक विशिष्ट उपयोग का मामला भी है । Data.Dataबॉयलरप्लेट बनाने के लिए टेम्प्लेट हास्केल के बजाय यह जेनरिक का उपयोग कर सकता है , इसलिए यदि आप 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कि @ ची के जवाब में है:

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

यह @ ची के टेम्पलेट हास्केल समाधान से कम नहीं है। एक संभावित लाभ यह है कि 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]
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.