简体   繁体   中英

map function to newtype defined as a List of Data structures

I'm new to Haskell and FP and am working my way through LYAH and other sources, but to "learn by doing" I'm trying to write a small program involving JSON parsing. However, I've coded myself into a corner and can't get out. My code is cobbled together from various tutorials and I can sense I'm still "thinking procedurally" about how to put it together nicely, but I haven't gotten to the needed breakthrough to make it work.

First, here is a chopped-down version of the multi-level JSON file, it's a weather forecast from the Weather Underground API, reduced to three hours.

{
    "response": {
        "version": "0.1",
        "termsofService": "http://www.wunderground.com/weather/api/d/terms.html",
        "features": {
            "hourly": 1
        }
    },
    "hourly_forecast": [{
        "FCTTIME": {
            "hour": "8",
            "epoch": "1479736800",
            "pretty": "8:00 AM CST on November 21, 2016"
        },
        "temp": {
            "english": "27",
            "metric": "-3"
        },
        "condition": "Partly Cloudy"
    }, {
        "FCTTIME": {
            "hour": "9",
            "epoch": "1479740400",
            "pretty": "9:00 AM CST on November 21, 2016"
        },
        "temp": {
            "english": "32",
            "metric": "0"
        },
        "condition": "Partly Cloudy"
    }, {
        "FCTTIME": {
            "hour": "10",
            "epoch": "1479744000",
            "pretty": "10:00 AM CST on November 21, 2016"
        },
        "temp": {
            "english": "35",
            "metric": "2"
        },
        "condition": "Clear"
    }]
}

Next, here is my Haskell program. I'm successfully parsing the JSON into a newtype called ForecastPointCollection which is defined as a List of WeatherPoint , which is a data structure of various things that came from the JSON file. But then, I can't figure out how to get the [WeatherPoint] list back out (see code comments). As a test of "something to do" with the list, I want to convert the Celcius temperature to Kelvin and get a new List that I can work with (output to JSON, do a show on, whatever).

{-# LANGUAGE OverloadedStrings   #-}
-- {-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Main where

import           Data.Aeson
import           Data.Aeson.Types

import           Control.Applicative  ((<$>), (<*>))
import           Control.Monad        (mzero)

import qualified Data.ByteString.Lazy as BSL
import qualified Data.Text            as T
import qualified Data.Vector          as V

type MetricTemperature = Int
type KelvinTemperature = Int

newtype ForecastPointCollection = ForecastPointCollection
  {forecastpointcollection :: [WeatherPoint]} deriving Show

data WeatherPoint = WeatherPoint
  { epoch      :: T.Text
  , prettyTime :: T.Text
  , tempMetric :: MetricTemperature
  , condition  :: T.Text
  } deriving Show

instance FromJSON ForecastPointCollection where
  parseJSON (Object o) =
    ForecastPointCollection <$> o .: "hourly_forecast"
  parseJSON _ = mzero

data ProcessedWeatherPoint = ProcessedWeatherPoint
  { newEpoch      :: T.Text
  , newPrettyTime :: T.Text
  , newTempKelvin :: KelvinTemperature
  , newCondition  :: T.Text
  } deriving Show

instance FromJSON WeatherPoint where
  parseJSON =
    withObject "Root Object Arbitrary Name" $ \o -> do
    fctO <- o .: "FCTTIME"
    epoch <- fctO .: "epoch" -- contained within FCTTIME
    pretty <- fctO .: "pretty" -- contained within FCTTIME
    tempO <- o .: "temp"
    metric <- tempO .: "metric" -- contained within temp
    condition <- o .: "condition" -- at top level under hourly_forecast
    return $ WeatherPoint epoch pretty (read metric) condition
  -- parseJSON _ = mzero

kelvinizeTemp :: MetricTemperature -> KelvinTemperature
kelvinizeTemp x = x + 273 -- hey, close enough

adjustTemp :: Maybe ForecastPointCollection -> [ProcessedWeatherPoint]
adjustTemp Nothing = []
adjustTemp x = [] -- HERE IS WHERE I AM LOSING MY WAY!
                  -- HOW CAN I WALK THROUGH THE LIST INSIDE ForecastPointCollection
                  -- TO map kelvinizeTemp ACROSS THAT LIST AND
                  -- GET A [ProcessedWeatherPoint] LIST BACK TO PLAY WITH?

getSampleForecast = BSL.readFile "/home/mypath/test/forecastsubmit.json"

main = do
  textOfJson <- getSampleForecast
  let (forecasts2 :: Maybe ForecastPointCollection) = decode textOfJson
  case forecasts2 of
    Just (ForecastPointCollection forecasts2) -> do
         putStrLn ("Success!")
         putStrLn . show  $ forecasts2
    _ -> putStrLn "Could not parse ForecastPointCollection JSON correctly."
  -- So far so good, we've extracted data from the JSON and stored it in memory.
  -- But now, how can we manipulate that data and start doing stuff with it?
  -- Currently, the "adjustTemp" function returns an empty list no matter what.
  let (processed2 :: [ProcessedWeatherPoint]) = adjustTemp forecasts2
  putStrLn ("More success (OK, not really, yet)!")
  putStrLn . show  $ processed2

Any advice appreciated. Should I not make ForecastPointCollection a newtype ? Where am I being idiomatic and where am I just idiotic? :-p

Update based on answer: For posterity, here is a possible (working) implementation of the newly-defined processWeatherPoint function. The pieces of a data structure should be thought of as a function!

processWeatherPoint :: WeatherPoint -> ProcessedWeatherPoint
processWeatherPoint x = ProcessedWeatherPoint
  (epoch x)
  (prettyTime x)
  (kelvinizeTemp (tempMetric x))
  (condition x)

kelvinizeTemp :: MetricTemperature -> KelvinTemperature
kelvinizeTemp x = x + 273 -- this works OK because both types are type of Int

It should be enough to define a function...

processWeatherPoint :: WeatherPoint -> ProcessedWeatherPoint

... extract the field with the [WeatherPoint] from the newtype and map the function over the list:

adjustTemp :: Maybe ForecastPointCollection -> [ProcessedWeatherPoint]
adjustTemp Nothing = []
adjustTemp (Just (ForecastPointCollection points)) = processWeatherPoint <$> points

An alternative to pattern matching on the ForecastPointCollection is using the record accessor for the field. That would be specially useful if you didn't intend to export the constructor:

adjustTemp :: Maybe ForecastPointCollection -> [ProcessedWeatherPoint]
adjustTemp Nothing = []
adjustTemp (Just forecast) = processWeatherPoint <$> forecastpointcollection forecast

An arguably more convenient way of writing the definition just above involves using the maybe function instead of doing explicit case analysis on Maybe :

adjustTemp :: Maybe ForecastPointCollection -> [ProcessedWeatherPoint]
adjustTemp = maybe [] (fmap processWeatherPoint . forecastpointcollection)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM