Adventures in pretty printing JSON in haskell
Today I gave atom haskell-ide a whirl and wanted to play with haskell a bit more. I've played with haskell in the past and always been put off by the tooling. To be fair, I still kind of am. I love the idea of the language but the tooling is just not there to make it an enjoyable exploratory experience. I spend half my time in the repl inspecting types, the other half on hoogle, and the 3rd half (yes I know) being frustrated that I can't just type in package names and explore API's in sublime or atom or wherever I am. Now that I'm on a mac, maybe I'll give leksah another try. I tried it a while ago it didn't work well.
Anyways, I digress. Playing with haskell and I thought I'd try poking with Aeson, the JSON library. Like Scala, you have to define your objects as json parseable/serializable (unlike java/c# which use runtime reflection). Thankfully if you enable some language extensions its just a matter of adding Generic
to your derives list and making sure the data type is of the ToJSON
and FromJSON
typeclasses. Mostly I was just copying the examples from here.
My sample class is
{-# LANGUAGE DeriveGeneric #-}
module Types where
import Data.Aeson
import GHC.Generics
data Person =
Person { firstName :: String
,lastName :: String
} deriving(Show, Generic)
instance ToJSON Person
instance FromJSON Person
And I just wanted to make a simple hello world where I'd read in some data, make my object, and print pretty json to the screen.
On my first try:
import Data.Aeson
import Types
process :: IO String
process = getLine
main = do
putStrLn "First name"
firstName \<- process
putStrLn "Last name"
lastName \<- process
let person = Person firstName lastName
print $ (encode person)
return ()
When I run cabal build;cabal run
I now get
First name
anton
Last name
kropp
"{\"lastName\":\"kropp\",\"firstName\":\"anton\"}"
Certainly JSON, but I want it pretty. I found aeson-pretty and gave that a shot. Now I'm doing:
import Data.Aeson
import Data.Aeson.Encode.Pretty
import Types
process :: IO String
process = getLine
main = do
putStrLn "First name"
firstName \<- process
putStrLn "Last name"
lastName \<- process
let person = Person firstName lastName
print $ (encodePretty person)
return ()
And I got:
First name
anton
Last name
kropp
"{\n \"lastName\": \"kropp\",\n \"firstName\": \"anton\"\n}"
Hmm. I can see that it should be pretty, but it isn't. How come? Lets check out the types:
Prelude \> import Data.Aeson
Prelude Data.Aeson \> :t encode
encode :: ToJSON a =\> a -\> Data.ByteString.Lazy.Internal.ByteString
Whats a lazy bytestring?
Well, from fpcomplete
ByteString provides a more efficient alternative to Haskell's built-in String which can be used to store 8-bit character strings and also to handle binary data
And the lazy one is the, well, lazy version. Through some googling I find that the right way to print the bytestring is by using the "putStr" functions in the Data.ByteString package. But as a good functional programmer, I want to encapsulate that and basically make a useful function that given the json object I can get a plain ol happy string and decide how to print it later.
I need to somehow make a lazy bytestring into a regular string. This leads me to this:
getJson :: ToJSON a =\> a -\> String
getJson d = unpack $ decodeUtf8 $ BSL.toStrict (encodePretty d)
So I first evaluate the bytestring into a strict version (instead of lazy), then decode it to utf8, then unpack the text class into strings (apparenlty text is more efficient but more API's use String).
Prelude \> :t toStrict
toStrict :: ByteString -\> Data.ByteString.Internal.ByteString
Prelude \> :t decodeUtf8
decodeUtf8 :: Data.ByteString.Internal.ByteString -\> Text
Prelude \> :t Data.Text.unpack
Data.Text.unpack :: Text -\> String
And now, finally:
import Data.Aeson
import Data.Aeson.Encode.Pretty
import qualified Data.ByteString.Lazy as BSL
import Data.Text
import Data.Text.Encoding
import Types
process :: IO String
process = getLine
getJson :: ToJSON a =\> a -\> String
getJson d = unpack $ decodeUtf8 $ BSL.toStrict (encodePretty d)
main = do
putStrLn "First name"
firstName \<- process
putStrLn "Last name"
lastName \<- process
let person = Person firstName lastName
print $ getJson person
return ()
Which gives me
First name
anton
Last name
kropp
"{\n \"lastName\": \"kropp\",\n \"firstName\": \"anton\"\n}"
AGHH! Still! Ok, more googling. Google google google.
Last piece of the puzzle is that print is really putStrLn . show
Prelude \> let x = putStrLn . show
Prelude \> x "foo\nbar"
"foo\nbar"
And if we just do
Prelude \> putStrLn "foo\nbar"
foo
bar
The missing ticket. Finally all put together:
import Data.Aeson
import Data.Aeson.Encode.Pretty
import qualified Data.ByteString.Lazy as BSL
import Data.Text
import Data.Text.Encoding
import Types
process :: IO String
process = getLine
getJson :: ToJSON a =\> a -\> String
getJson d = unpack $ decodeUtf8 $ BSL.toStrict (encodePretty d)
main = do
putStrLn "First name"
firstName \<- process
putStrLn "Last name"
lastName \<- process
let person = Person firstName lastName
putStrLn $ getJson person
return ()
Which gives me:
$ cabal build; cabal run
Building sample-0.1.0.0...
Preprocessing executable 'sample' for sample-0.1.0.0...
[3 of 3] Compiling Main ( src/Main.hs, dist/build/sample/sample-tmp/Main.o )
Linking dist/build/sample/sample ...
Preprocessing executable 'sample' for sample-0.1.0.0...
Running sample...
First name
anton
Last name
kropp
{
"lastName": "kropp",
"firstName": "anton"
}
Source available at my github