[Haskell-cafe] The Good, the Bad and the GUI

John Lato jwlato at gmail.com
Thu Aug 14 00:21:28 UTC 2014


On Wed, Aug 13, 2014 at 4:21 PM, Tom Ellis <
tom-lists-haskell-cafe-2013 at jaguarpaw.co.uk> wrote:

> On Wed, Aug 13, 2014 at 10:31:31PM +0200, Wojtek Narczyński wrote:
> > On 13.08.2014 12:37, Tom Ellis wrote:
> > >On Tue, Aug 12, 2014 at 12:46:05PM +0200, Wojtek Narczyński wrote:
> > >>Continuing my VAT Invoice example, let us say a LineItem that does
> > >>not have a product description (missing value), but it does have all
> > >>the numeric fields filled in.  It is partly erroneous, but it can be
> > >>included in calculation of the total. How would you handle it with
> > >>Either [Error] Invoice style code?
> > >What sort of functionality are you looking for exactly?  What's your
> > >objection to (for example)
> > >
> > >     data LineItemGeneral a = LineItem { price :: Price
> > >                                       , quantity :: Quantity
> > >                                       , description :: a }
> > >
> > >     type LineItem = LineItemGeneral String
> > >     type LineItemPossiblyIncomplete = LineItemGeneral (Maybe String)
> > >     type LineItemWithoutDescription = LineItemGeneral ()
> > >
> > >     totalValue :: LineItemGeneral a -> Value
> > >     totalValue lineItem = price lineItem * quantity lineItem
> > >
> > >`totalValue` works for all sorts of line items, whether they have a
> > >description or not.
> >
> > Let's say the user entered:
> >
> > No, Name, Qty, Price
> > --------------------------------------------
> > 1. [        ]   [99] [10]
> > 2. [Water] [    ] [10]
> > 3. [Juice]   [  1] [    ]
> >
> > The GUI should display total of 990, and signal four errors: three
> > missing values (ideally different color of the input fields), and
> > the whole invoice incomplete. The Either [Error] Invoice type does
> > not work, because can either display the errors or calculate total
> > from a correct invoice, never both. And you can't even create
> > LineItem for 2. and 3. Well, maybe you can with laziness, but how
> > would total work then?
> >
> > That's why I asked in my original post, whether I'd need two types,
> > one for correct complete invoice, and another for the invoice "in
> > statu nascendi". And how to obtain them, lazily, and I mean the
> > person, not the language.
>
> Perhaps I don't grasp exactly what you're getting at, but this seems easy.
> Please let me know where my proposed solution fails to provide what you
> need.
>
> I do see that you originally said "In Haskell you'd need two data types:
> the
> usual proper Haskell data type, and another which wraps every field in
> Maybe, facilitates editing, validation, etc.".  You don't actually *need*
> the version without the Maybe, but you can provide it if you want some
> additional type safety.  If you'd like to see an example of making that
> nice and easy with minimal boilerplate please ask.
>
>
>     import Control.Applicative
>     import Data.Maybe
>     import Control.Arrow
>
>     type Quantity = Double
>     type Price = Double
>     type Value = Double
>
>     data LineItem = LineItem { name :: Maybe String
>                              , quantity :: Maybe Quantity
>                              , price :: Maybe Price }
>


Rather than this definition, what about something like:

    data LineItemF f = LineItem
        { name :: f String
        , quantity :: f Quantity
        , price :: f Price }

    type LineItemBuilder = LineItemF (Writer Error)
    type LineItem = LineItemF Identity

    newLineItemBuilder :: LineItemBuilder
    newLineItemBuilder = LineItemF
        {"Missing" <$ tell (Error 1 NameField)
        ,0 <$ tell (Error 2 QuantityField)
        ,0 <$ tell (Error 3 PriceField)}

    setName :: LineItemBuilder -> String -> LineItemBuilder
    setName li newName = if validName newName
        then li { name = pure newName }
        else li -- either leave the original, or add the new name and tell
another error,
                  -- depending on use case

    -- quantity,price can be set similarly

    buildLineItem :: LineItemBuilder -> Either [Error] LineItem
    buildLineItem LineItemF{name, quantity,price} = case runWriter builder
of
         (built,[]) -> Right built
         (_, errs) -> Left errs
      where
        builder = LineItemF <$> (pure <$> name)
                                     <*> (pure <$> quantity)
                                     <*> (pure <$> price)

Now you have one type that represents a LineItem, and you can determine the
state of the LineItem by which functor is used.  You'll probably be able to
get some code re-use for any functions that don't need to know if a
particular LineItem is valid or not, but there's still a type-level
distinction between validated and unvalidated LineItems.  And if you're
using lens, you can access the component fields with "name . _Wrapped" (or
maybe _Unwrapped, depends on which version of lens you're using).

If you're taking arbitrary strings as user input, and they haven't been
parsed as numbers yet (or otherwise validated), you can even handle that
case by using an appropriate functor, such as "Constant String".  Then you
could have a function like

    validate :: (String -> Either ValidationError a) -> Constant String a
-> Either ValidationError a

that takes a parser and parses/validates the field.

John L.


>
>     data Field = NameField | QuantityField | PriceField
>                deriving Show
>
>     data Error = Error { item :: Int
>                        , missing :: Field }
>                deriving Show
>
>     value :: LineItem -> Maybe Value
>     value l = (*) <$> price l <*> quantity l
>
>     totalValue :: [LineItem] -> Value
>     totalValue = sum . map (fromMaybe 0 . value)
>
>     missingFields :: LineItem -> [Field]
>     missingFields l = n ++ q ++ p
>       where n = if name l == Nothing then [NameField] else []
>             q = if quantity l == Nothing then [QuantityField] else []
>             p = if price l == Nothing then [PriceField] else []
>
>     errors :: [LineItem] -> [Error]
>     errors = concatMap (\(i, es) -> map (Error i) es)
>              . zip [1..]
>              . map missingFields
>
>     guiResponse :: [LineItem] -> (Value, [Error])
>     guiResponse = totalValue &&& errors
>
>     exampleData :: [LineItem]
>     exampleData = [ LineItem Nothing        (Just 99) (Just 10)
>                   , LineItem (Just "Water") Nothing   (Just 10)
>                   , LineItem (Just "Juice") (Just 1)  Nothing ]
>
>     -- *Main> guiResponse exampleData
>     -- (990.0, [ Error {item = 1, missing = NameField}
>     --         , Error {item = 2, missing = QuantityField}
>     --         , Error {item = 3, missing = PriceField}])
> _______________________________________________
> Haskell-Cafe mailing list
> Haskell-Cafe at haskell.org
> http://www.haskell.org/mailman/listinfo/haskell-cafe
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://www.haskell.org/pipermail/haskell-cafe/attachments/20140813/ab6e441d/attachment.html>


More information about the Haskell-Cafe mailing list