9
votes

I'm a fairly new Haskell programmer and I'm trying to figure out how to get some values into an algebraic data type.

I have a record data type:

data OrbitElements = OrbitElements { epoch :: Double,
                                     ecc :: Double,
                                     distPeri :: Double,
                                     incl :: Double,
                                     longAscNode :: Double,
                                     argPeri :: Double,
                                     timePeri :: Double,
                                     meanMotion :: Double,
                                     meanAnomaly :: Double,
                                     trueAnomaly :: Double,
                                     semiMajorAxis :: Double,
                                     distApo :: Double,
                                     period :: Double
                                   }

I'm pulling in some info from a text file, which ends up in a list of Doubles. Is there an easy way to initialize this data type with the list? I could just call each setter individually but that seems terribly inefficient when I already have all the values in a list.

let d = [2456382.5,6.786842103348031e-3,0.7184187640759256,3.394660181513041,76.64395338801751,55.2296201483587,2456457.141012543,1.602144936476915,240.4142797010899,239.7408018186761,0.7233278761603762,0.7282369882448266,224.6987721295883]
let o = OrbitElements
let epoch o = d !! 0
let ecc o = d !! 1
-- and so on

What am I missing?

4

4 Answers

16
votes

The most direct way is to just do it by hand:

fromList :: [Double] -> Maybe OrbitElements
fromList [ _epoch
         , _ecc
         , _distPeri
         , _incl
         , _longAscNode
         , _argPeri
         , _timePeri
         , _meanMotion
         , _meanAnomaly
         , _trueAnomaly
         , _semiMajorAxis
         , _distApo
         , _period
         ]
    = Just $ OrbitElements
          _epoch
          _ecc
          _distPeri
          _incl
          _longAscNode
          _argPeri
          _timePeri
          _meanMotion
          _meanAnomaly
          _trueAnomaly
          _semiMajorAxis
          _distApo
          _period
fromList _ = Nothing

However, there is a slightly sexier way, which is to parse them element by element, which is less error-prone and more descriptive of what we are trying to do:

First we define two parsers, one of which requests a new element from the list (or fails if the list is empty), and the second of which matches the end of the list (or fails if the list is not empty):

import Control.Applicative
import Control.Monad
import Control.Monad.Trans.State

getElem :: StateT [Double] Maybe Double
getElem = do
    s <- get
    case s of
        []   -> mzero
        x:xs -> do
            put xs
            return x

endOfList :: StateT [Double] Maybe ()
endOfList = do
    s <- get
    case s of
        [] -> return ()
        _  -> mzero

Now we can define fromList in Applicative style:

fromList' :: [Double] -> Maybe OrbitElements
fromList' = evalStateT $ OrbitElements
    <$> getElem
    <*> getElem
    <*> getElem
    <*> getElem
    <*> getElem
    <*> getElem
    <*> getElem
    <*> getElem
    <*> getElem
    <*> getElem
    <*> getElem
    <*> getElem
    <*> getElem
    <*  endOfList
11
votes

A somwhat ugly solution... :-o

Make your type derive Read:

data OrbitElements = OrbitElements { ... }
                         deriving (Read)

Then you can define fromList by

fromList :: [Double] -> OrbitElements
fromList ds = read $ "OrbitElement " ++ (concat $ Data.List.intersperse " " $ map show ds)
5
votes

You're missing the fact that Haskell is statically typed. No, Haskell doesn't have any such construct.

Let's suppose the language had some way to fill the constructors values from list. Here are some questions to think about:

  • What would happen when if the list contained more or less items than required?
  • How would you initialize a record whose fields were not uniformly typed?
2
votes

Algebraic data types are meant to be initialized all at once, not a field at a time as you're doing. The correct way to do this is:

let d = ...
let o = OrbitElements {epoch = d !! 0
                       ecc = d !! 1,
                       distPeri = d !! 2,
                       incl = d !! 3,
                       longAscNode = d !! 4,
                       argPeri = d !! 5,
                       timePeri = d !! 6,
                       meanMotion = d !! 7,
                       meanAnomaly = d !! 8,
                       trueAnomaly = d !! 9,
                       semiMajorAxis = d !! 10,
                       distApo = d !! 11,
                       period = d !! 12}

Note that the way you're doing it isn't actually setting any values in o. let epoch o = d !! 0 defines a function named epoch that masks the definition of epoch as a field (You should always compile with warnings enabled so that the compiler will catch things like this), and this new function takes any value o (not just the OrbitElements previously defined) and returns d !! 0 without doing anything with o at all. If you actually wanted to set or change the epoch field of o, the correct way to do it would be let o' = o {epoch = d !! 0}, which returns a new OrbitElements object with its epoch field changed.