Haskell - Aeson : Getting “Nothing” when trying to

2019-09-02 07:52发布

I'm relatively new to haskell and right now I'm trying to get a deeper understanding and trying to get used to different popular libraries.

Right now I'm trying "aeson".

What I want to do is parse MSFT quote request from https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=MSFT&apikey=demo

This is what it looks like

{
    "Global Quote": {
        "01. symbol": "MSFT",
        "02. open": "105.3500",
        "03. high": "108.2400",
        "04. low": "105.2700",
        "05. price": "107.6000",
        "06. volume": "23308066",
        "07. latest trading day": "2018-10-11",
        "08. previous close": "106.1600",
        "09. change": "1.4400",
        "10. change percent": "1.3564%"
    }
}

This is what I've got so far

{-# LANGUAGE DeriveGeneric     #-}
{-# LANGUAGE OverloadedStrings #-}

import           Data.Aeson
import qualified Data.ByteString.Lazy as B
import           GHC.Exts
import           GHC.Generics
import           Network.HTTP
import           Network.URI

jsonURL :: String
jsonURL = "http://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=MSFT&apikey=demo"

getRequest_ :: HStream ty => String -> Request ty
getRequest_ s = let Just u = parseURI s in defaultGETRequest_ u

jsonReq = getRequest_ jsonURL

data Quote = Quote {quote         :: String,
                    symbol        :: String,
                    open          :: Float,
                    high          :: Float,
                    low           :: Float,
                    price         :: Float,
                    volume        :: Float,
                    ltd           :: String,
                    previousClose :: Float,
                    change        :: Float,
                    changePerct   :: Float
                   } deriving (Show, Generic)

instance FromJSON Quote
instance ToJSON Quote


main :: IO ()
main = do
  d <- simpleHTTP jsonReq
  body <- getResponseBody d
  print (decode body :: Maybe Quote)

What am I doing wrong?

Edit: Fixed version in the answers.

2条回答
ゆ 、 Hurt°
2楼-- · 2019-09-02 07:58

First off: Aeson is not the easiest library for a beginner. There are more difficult ones, sure, but it supposes you already a fair number of things about the language. You didn't pick the "simplest task" to begin with. I know this can be surprising, and you might think that parsing JSON should be simple, but parsing JSON with strong type guarantees is actually not that simple.

But here's what I can tell you to help you a bit:

  • First, use eitherDecode rather than decode: you will get an error message rather than simply Nothing, which will help you a bit.

  • Deriving through Generic is neat and very often, a time saver, but it's not magic either. The name of the object key and the name of your datatype fields have to match exactly. Sadly, this is not the case here and due to haskell syntax, you couldn't name your fields like the keys of the object. Your best solution is to implement FromJSON manually (see the recommended link below). A good way to see "what is expect" by the generic FromJSON is to also derive ToJSON, create a dummy Quote and see the result of encode.

  • Your first field (quote) is not a key of the object itself, but rather the name of this object. So you have dynamic keys ("Global Quote" being one here). Once again, this typically a case where you want to write the FromJSON instance manually.

I recommend you read this famous tutorial written by Artyom Kazak on Aeson. This will help you tremendously and is probably the best advice I can give.

For your manual instance, supposing it was exactly the document you want to parse and you had only the "Global Quote" to deal with, it would look more or less like this:

instance ToJSON Quote where
  parseJSON = withObject "Document" $
    \d -> do
        glob <- d .: "Global Quote"
        withObject "Quote" v (\gq ->
          Quote <$> gq .: "01. symbol"
                <*> pure "Global Quote"
                <*> gq .: "02. open"
                <*> gq .: "03. high"
          -- ... and so on
         ) v

(It's not the most pretty way, nor the best way, to write it, but it should be one possible way).

Also note that, as an astute commenter wrote, the types of your fields are not always aligned with the type of your example JSON document. "volume" is an Int (byte-limited int), potentially an Integer ("mathematical" integer, no bound), but not a Float. Your "ltd" can be parsed a string - but it probably should be a date (Day from Data.Time would be the first choice - it already has a FromJSON instance so chances are it should be parseable as is). Change percent is most likely not parseable as Float like that, you'll need to write a dedicated parser for this type (and decide how you want to store it - Ratio is a potential solution).

查看更多
再贱就再见
3楼-- · 2019-09-02 07:59

@Raveline with their answer above pointed me to the right direction. I was able to solve all those issues, here's the final product!

{-# LANGUAGE DeriveGeneric     #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards   #-}

module Test where

import           Data.Aeson
import qualified Data.ByteString.Lazy as B
import           GHC.Exts
import           GHC.Generics
import           Network.HTTP.Conduit (simpleHttp)

jsonURL :: String
jsonURL = "https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=MSFT&apikey=demo"

getJSON :: IO B.ByteString
getJSON = simpleHttp jsonURL

data Quote = Quote {
                   symbol        :: String,
                   open          :: String,
                   high          :: String,
                   low           :: String,
                   price         :: String,
                   volume        :: String,
                   ltd           :: String,
                   previousClose :: String,
                   change        :: String,
                   changePercent :: String
                   } deriving (Show, Generic)


instance FromJSON Quote where
  parseJSON = withObject "Global Quote" $
    \o -> do
      globalQuote   <- o           .: "Global Quote"
      symbol        <- globalQuote .: "01. symbol"
      open          <- globalQuote .: "02. open"
      high          <- globalQuote .: "03. high"
      low           <- globalQuote .: "04. low"
      price         <- globalQuote .: "05. price"
      volume        <- globalQuote .: "06. volume"
      ltd           <- globalQuote .: "07. latest trading day"
      previousClose <- globalQuote .: "08. previous close"
      change        <- globalQuote .: "09. change"
      changePercent <- globalQuote .: "10. change percent"
      return Quote {..}


main :: IO ()
main = do
  d <- (eitherDecode <$> getJSON) :: IO (Either String Quote)
  case d of
    Left e   -> print e
    Right qt -> print (read (price qt) :: Float)
查看更多
登录 后发表回答