Search code examples
parsinghaskellaeson

Expanding Aeson example, how to use array of values?


I am trying to understand a little more Haskell, and have followed an Aeson example with success. I'm trying to adapt it now, and am lacking and what I'm sure is a fairly basic understanding. If it matters, I am doing this in a Jupyter Lab setup using the IHaskell-aeson package.

The initial example assumed an input JSON file consisting of a single, simple object. The output parses the object and provides a small greet message.

{
  "name": "Johann Carl Freidrich Guass",
  "nationality": "German",
  "born": "30 April 1777",
  "died": "23 February 1855"
}

Output: Johann Carl Freidrich Guass was born 30 April 1777

I want to extend this to use this JSON:

{
  "mathematicians": [
    { "name".... },
    { "name".... },
    { "name".... },
    { "name".... }
  ]
}

Here is my code right now, I've left commented in the first version (:extension is for IHaskell/Jupyter):

:extension OverloadedStrings

import qualified Data.ByteString.Lazy as B
import Control.Monad (mzero)
import Data.Foldable (toList)
import Data.Aeson.Types (Parser)

data Mathematicians = Mathematicians
                    { mathematicians :: [Mathematician]
                    } deriving (Show)
data Mathematician = Mathematician 
                     { name :: String
                     , nationality :: String
                     , born :: String
                     , died :: Maybe String
                     } deriving (Show)
                     
instance A.FromJSON Mathematician where
    parseJSON (A.Object v) = Mathematician
                         <$> (v A..: "name")
                         <*> (v A..: "nationality")
                         <*> (v A..: "born")
                         <*> (v A..:? "died")
    parseJSON _ = mzero
                     
instance A.FromJSON Mathematicians where
    parseJSON (A.Object v) = do
        ms <- v A..: "mathematicians"
        fmap Mathematicians $ A.parseJSON ms
    parseJSON _ = mzero
                         
input <- B.readFile "mathematicians.json"

greet m = (show.name) m ++ 
          " was born " ++ 
          (show.born) m

-- let mm = A.decode input :: Maybe Mathematician
-- case mm of
--     Nothing -> print "error parsing JSON"
--     Just m -> (putStrLn.greet) m

let mms = A.decode input :: Maybe Mathematicians
case mms of
    Nothing -> print "error parsing JSON"
    Just ms -> print ms

Printing ms gives me the following, which leads me to believe the parsing is probably working:

Mathematicians {mathematicians = [Mathematician {name = "Johann Carl Friedrich Gauss", nationality = "German", born = "30 April 1777", died = Just "23 February 1855"},Mathematician {name = "Leonhard Euler", nationality = "German", born = "15 April 1707", died = Just "18 September 1783"},Mathematician {name = "William Rowan Hamilton", nationality = "Irish", born = "3 August 1805", died = Just "2 September 1865"},Mathematician {name = "Terence Chi-Shen Tao", nationality = "Australia", born = "17 July 1975", died = Nothing}]}

Also, intentionally inserting a typo in the Mathematician parser leads to an error parsing JSON message, so fingers crossed it does seem like the parsing works.

However, I do not understand how to access the individual Mathematician elements, and then run greet on them sequentially.

I have tried printing like this: print (fmap greet ms) and get

<interactive>:3:34: error:
    • Couldn't match expected type ‘f0 Mathematician’ with actual type ‘Mathematicians’
    • In the second argument of ‘fmap’, namely ‘ms’
      In the first argument of ‘print’, namely ‘(fmap greet ms)’
      In the expression: print (fmap greet ms)

using print (map greet ms) instead:

<interactive>:3:33: error:
    • Couldn't match expected type ‘[Mathematician]’ with actual type ‘Mathematicians’
    • In the second argument of ‘map’, namely ‘ms’
      In the first argument of ‘print’, namely ‘(map greet ms)’
      In the expression: print (map greet ms)

neither of which, I'm sorry to say, I understand at all.


Solution

  • You can make use of mapM_ :: (Monad m, Foldable f) => (a -> m b) -> f a -> m () to run a monadic action on all the elements of a Foldable:

    greet :: Mathematician -> String
    greet Mathematician { name=n, born=b } = n ++ " was born " ++ show b
    
    main = do
        input <- B.readFile "mathematicians.json"
        case A.decode input :: Maybe Mathematicians of
        Nothing -> print "error parsing JSON"
        Just (Mathematicians ms) -> mapM_ (putStrLn . greet) ms