简体   繁体   中英

Combining functions in Haskell

How can I combine these similar function in Haskell?

getGroup [] acc = reverse acc
getGroup ((Letter x):letfs) acc = getGroup letfs ((Letter x):acc)
getGroup ((Group x):letfs) acc = getGroup letfs ((Group (makeList x)):acc)
getGroup ((Star x):letfs) acc = getGroup letfs ((Star (makeList x)):acc)
getGroup ((Plus x):letfs) acc = getGroup letfs ((Plus (makeList x)):acc)
getGroup ((Ques x):letfs) acc = getGroup letfs ((Ques (makeList x)):acc)

Letter, Group, Star, Plus and Ques are all part of a data type definition.

data MyData a 
        = Operand a 
        | Letter a
        | Star [RegEx a] 
        | Plus [RegEx a] 
        | Ques [RegEx a]
        | Pipe [RegEx a] [RegEx a]
        | Group [RegEx a]
        deriving Show

I wonder if there is a better way to write those function because of their similarities. Mostly, I wish to combine the functions for Group, Star, Plus and Ques, because they are identical, but if there is a method to combine all of them it would be better.

You can't get rid of the repetition of the pattern matching without using Template Haskell, which probably isn't worth it for only five different constructors. You can eliminate a lot of the other repetition though, and improve the performance characteristics of the function, as well.

getGroup = map go
  where go (Letter x) = Letter x
        go (Group x) = Group . makeList $ x
        go (Star x) = Star . makeList $ x
        go (Plus x) = Plus . makeList $ x
        go (Ques x) = Ques . makeList $ x

In addition to being much more concise, it also gets rid of the tail-recursion that will cause a space leak in a lazy language like Haskell.

When you have a data type definition defined as you have, as a disjoint union of several different cases, you will inevitably end up with a large mess of case analysis in functions that process that type.

One approach to reducing the case analysis would be to simplify the base type by factoring out the commonality:

data MyData a = Val String a 
              | UnOp String [Regex a]
              | BinOp String [Regex a] [Regex a]

In this formulation, each case has a discriminator field with which you can tell apart the different kinds of each case. Here, I just used String assuming you'd give them names like "Operand", "Letter", "Star", etc., but you could also define separate enumeration types for the valid discriminators for kinds of Val , kinds of UnOp , etc.

The main thing you lose in this case is type safety; you could construct especially nonsensical things with the String fields as I gave them. A first approach to tackling this problem is to use what are known as smart constructors ; these are functions with specifically-typed parameters that build the more weakly-typed core data in a type-safe manner. As long as you do not export the actual MyData constructors from your module, other users of your type will only be able to construct sensible data via your smart constructors.

If you want more guarantees of safe construction from the type constructors themselves, you would want to turn to the concepts of Generalized Algebraic Data Types (GADTs) and Phantom Types . The basic idea behind these is to have more flexible relationship between the type variables on the left-hand side of the = of a data type definition and the type variables on the right-hand side. They are a somewhat new and advanced feature of Haskell, though, so you may want to hold off on jumping into them until you have a firm grasp on standard Haskell data types.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM