A data type with a default field, and it needs a function to work with it

Let's say I have a data type

data FooBar a = Foo String Char [a]
              | Bar String Int [a]

      

I need to create values ​​of this type and give an empty list as the second field:

Foo "hello" 'a' []

      

or

Bar "world" 1 []

      

1) I do this everywhere in my code and I think it would be nice if I somehow omit the empty part of the list and not have the empty list assigned implicitly. Is it possible? Something similar to default function arguments in other languages.

2) Because of this []

"default" value , I often need to have a partial constructor application that results in a function that takes the first two values:

mkFoo x y = Foo x y []
mkBar x y = Bar x y []

      

Is there a "better" (more idiomatic, etc.) way to do this? to avoid defining new functions?

3) I need a way to add things to the list:

add (Foo u v xs) x = Foo u v (x:xs)
add (Bar u v xs) x = Bar u v (x:xs)

      

Is this the way it is done idiomatically? Just a general purpose function?

As you can see, I'm a beginner, so maybe these questions don't make any sense. I hope no.

+3


source to share


3 answers


(2) and (3) are perfectly normal and idiomatic ways of doing such things. About (2) in particular, one expression that you sometimes hear is "smart constructor". This means that it is a function mkFoo

/ mkBar

that creates FooBar a

(or Maybe (FooBar a)

, etc.) with some extra logic to ensure that only sensible values ​​can be created.

Here are some additional tricks that may (or may not!) Make sense, depending on what you are trying to do with FooBar

.

If you use values Foo

and Bar

values ​​in a similar way most of the time (i.e. the difference between having a field Char

and Int

being a minor detail), it makes sense to factor from the general lines and use a single constructor:

data FooBar a = FooBar String FooBarTag [a]
data FooBarTag = Foo Char | Bar Int

      

Apart from preventing the parsing of cases where you don't care FooBarTag

, that allows for the safe use of the notation syntax (records and types with multiple constructors don't mix well).

data FooBar a = FooBar
    { fooBarName :: String
    , fooBarTag :: FooBarTag
    , fooBarList :: [a]
    }

      

Records allow you to use fields without a pattern to match everything.

If there are reasonable defaults for all fields in FooBar

, you can go one step beyond the constructors mkFoo

and define a default.

defaultFooBar :: FooBar a
defaultFooBar = FooBar
    { fooBarName = ""
    , fooBarTag = Bar 0
    , fooBarList = []
    }

      



You don't need records to use the default, but they allow you to conveniently override the default fields.

myFooBar = defaultFooBar
    { fooBarTag = Foo 'x'
    }

      

If you've ever gotten tired of typing long names by default, consider the data-default

package:

instance Default (FooBar a) where
    def = defaultFooBar

myFooBar = def { fooBarTag = Foo 'x' }

      

Note that a significant number of people dislike the classDefault

, and for good reason. However, for types that are very specific to your application (like config settings) Default

works fine IMO.

Finally, updating record fields can be messy. If you end up annoyed by this, you will find it lens

very helpful. Note that this is a large library and can be a bit overwhelming for a beginner, so take a deep breath beforehand. Here's a small example:

{-# LANGUAGE TemplateHaskell #-} -- At the top of the file. Needed for makeLenses.
import Control.Lens

-- Note the underscores.
-- If you are going to use lenses, it is sensible not to export the field names.
data FooBar a = FooBar
    { _fooBarName :: String
    , _fooBarTag :: FooBarTag
    , _fooBarList :: [a]
    }
makeLenses ''FooBar -- Defines lenses for the fields automatically. 

defaultFooBar :: FooBar a
defaultFooBar = FooBar
    { _fooBarName = ""
    , _fooBarTag = Bar 0
    , _fooBarList = []
    }

-- Using a lens (fooBarTag) to set a field without record syntax.
-- Note the lack of underscores in the name of the lens.
myFooBar = set fooBarTag (Foo 'x') defaultFooBar

-- Using a lens to access a field.
myTag = view fooBarTag myFooBar -- Results in Foo 'x'

-- Using a lens (fooBarList) to modify a field.
add :: a -> FooBar a -> FooBar a
add x fb = over fooBarList (x :) fb

-- set, view and over have operator equivalents, (.~). (^.) and (%~) respectively.
-- Note that (^.) is flipped with respect to view.

      

Here's a gentle introduction to lens

, focusing on aspects that I haven't demonstrated here, especially how to create lenses beautifully.

+1


source


I will solve your questions one by one.



  • There are no default arguments in Haskell. They just aren't worth the added complexity and loss of composition. As a functional language, you do a lot more function manipulation in Haskell, so fan coil units like default arguments will be difficult to handle.

  • One thing I didn't understand when I started Haskell is that data constructors are functions like everything else. In your example

    Foo :: String -> Char -> [a] -> FooBar a
    
          

    This way you can write functions to fill in various arguments of other functions, and then those functions will work with Foo or Bar or whatever.

    fill1 :: a -> (a -> b) -> b
    fill1 a f = f a
    --Note that fill1 = flip ($)
    
    fill2 :: b -> (a -> b -> c) -> (a -> c)
    --Equivalently, fill2 :: b -> (a -> b -> c) -> a -> c
    fill2 b f = \a -> f a b
    
    fill3 :: c -> (a -> b -> c -> d) -> (a -> b -> d)
    fill3 c f = \a b -> f a b c
    
    fill3Empty :: (a -> b -> [c] -> d) -> (a -> b -> d)
    fill3Empty f = fill3 [] f
    
    --Now, we can write 
    > fill3Empty Foo x y 
        Foo x y []
    
          

  • The lens package provides elegant solutions to such questions. However, you can tell at a glance that this package is extremely complex. Here is what you would call a lens kit:

    _list :: Lens (FooBar a) (FooBar b) [a] [b]
    _list = lens getter setter
      where getter (Foo _ _ as) = as
            getter (Bar _ _ as) = as
            setter (Foo s c _) bs = Foo s c bs
            setter (Bar s i _) bs = Bar s i bs
    
          

    Now we can do

    > over _list (3:) (Foo "ab" 'c' [2,1]) 
        Foo "ab" 'c' [3,2,1]
    
          

    Some explanation: a function lens

    creates a type lens

    if a getter and setter is given for some type. Lens s t a b

    is a type that says " s

    contains a

    and t

    contains a b

    . So if you give me a function a -> b

    , I can give you a function s -> t

    ." This is what it does over

    : you provide it with a lens and a function (in our case (3:)

    was a function that adds 3 to the front of the list), and it applies the "where the lens points" function. This is very similar to a functor, but we have a lot more freedom (in this example, the functor instance would have to modify every element of the lists, rather than work with the lists themselves).

    Note that our new lens _list is very generic: it works the same on Foo

    and Bar

    , and the lens package provides a lot of other than on functions over

    to do magic things.

+2


source


The idiomatic thing is to take those function or constructor parameters that you usually want to partially apply and move them to the beginning:

data FooBar a = Foo [a] String Char
              | Bar [a] String Int

foo :: String -> Char -> FooBar a
foo = Foo []

bar :: String -> Int -> FooBar a
bar = Bar []

      

Likewise, reordering of parameters before add

allows partial application add

to obtain functions of type FooBar a -> FooBar a

that are easy to compile:

add :: a -> FooBar a -> FooBar a
add x (Foo xs u v) = Foo (x:xs) u v

add123 :: FooBar Int -> FooBar Int
add123 = add 1 . add 2 . add 3

add123 (foo "bar" 42) == Foo [1, 2, 3] "bar" 42

      

+2


source







All Articles