To facilitate writing posts in my usual text editor, in markdown (finally, proper and painless code highlighting!), future content will be posted on Github Pages at https://tkuriyama.github.io/
A Free Education
Wednesday, January 20, 2021
Friday, November 13, 2020
Elm Codegen Quirks with Servant-Elm and Persistent
Developing further on the previous post, building some slightly less trivial examples with the combination of servant-elm and persistent (with Sqlite backend) yields some interesting problems in the generated Elm code.
1. Phantom Types for Keys
Due to the way persistent handles database primary keys, servant-elm (via the underlying elm-bridge) currently generates Elm code with types such as Key World instead of WorldId, where, for Sqlite, ID variables are of type Int, used for database primary keys.
But what is Key World in terms of Elm types? And what happens when you try to sequence a JSON encoder / decoder for Key (which doesn't exist!), along with a JSON encoder / ecoder for World -- something altogether different from the desired Int?
{ userEnv: (Key World)
, userName: String
[...]
jsonEncUser : User -> Value
jsonEncUser val =
Json.Encode.object
[ ("userEnv", (jsonEncKey (jsonEncWorld)) val.userEnv)
, ("userName", Json.Encode.string val.userName)
2. Missing URL String Encoders
I also couldn't get servant-elm to generate correct URL string encoders, despite defining instances of toHttpApiData, which seems like it should be the thing to do.
[...]
, url =
Url.Builder.crossOrigin "http://localhost:8080"
[ "cellColor"
, (capture_worldid |> String.fromInt)
, (capture_x |> String.fromInt)
, (capture_y |> String.fromInt)
, (capture_color |> String.fromInt)
]
[...
... where Color is defined on the Haskell side as:
= White
| Yellow
| Red
| Green
| Blue
| Grey
deriving (Show, Read, Eq, Generic, Enum, Bounded)
instance FromHttpApiData Color where
parseUrlPiece :: T.Text -> Either T.Text Color
parseUrlPiece = myParse
instance ToHttpApiData Color where
toUrlPiece = T.pack . show
toQueryParam = T.pack . show
Wrapping Up
With the above edits, the Elm code gen + persistence layer stack works again. There was a lot more research and work(arounds) required this time, and I needed some help from friends at the Recurse Center, but it still (mostly!) satisfies the property of writing data definitions only once for the whole stack. It's not hard to see that some of the type mapping issues across languages and data boundaries are non-trivial to resolve, so I hope more usage and development goes into these kinds of type-safe stacks.
Monday, November 9, 2020
Hello Server + Persistent
The previous post covered setting up a Servant server in Haskell, with automatic codegen for Elm bindings and using the acid-state package for persistence.
An obvious benefit of the stack is that one only ever needs to think in native Haskell data structures. It's relatively easy to use and achieves the goal of minimizing work across data boundaries (in this case, between the server and persistence layer). It's also self-contained, insofar as it doesn't require an external database server / process / engine.
On the other hand... many external database servers have been developed and refined for years -- if not decades -- yielding stability, tools, and all sorts of best practices and optimizations.
Yesod's answer to the problem is persistent, which provides a type-safe and unified way to interact with a variety of database backends (primarily Sqlite, MySQL, Postgres, and MongoDB). Although the library was designed for Yesod, it is not tightly coupled and seems to work for Servant.
This file contains the data definitions for a revised version of Hello Server. persist also uses Template Haskell, so we adapt the data declarations accordingly:
share | |
[mkPersist sqlSettings , mkMigrate "migrateAll"] | |
[persistLowerCase| | |
ServerState | |
state String | |
counter Int | |
deriving Eq Show Generic | |
|] |
Note that the servant-elm template invocations come later and works as expected -- i.e. first, persistent generates the ServerState data type and associate code it needs; then, servant-elm uses the generated ServerState data type for its purposes.
This file contains the server instance, following the example in the Servant repo.
And, with a few small changes on the Elm client side to accommodate API tweaks, it works!
There are a number of fundamental things I still haven't figured out (like... shouldn't the Sqlite connection pool be explicitly closed in the bracket pattern?). Overall, there are more things to think about compared to working with acid-state -- which seems reasonabl, since it corresponds to the relative power of the database engine (or at least, I assume so). Leveraging the "migrations" feature from persistent, though, I've still only written a single data definition in Haskell for the entire stack!
The repo with all examples: https://github.com/tkuriyama/spacemonkey
Thursday, November 5, 2020
Hello Server + Acid-State
The previous post covered defining a Servant server with automatic code generation for Elm bindings.
Are there libraries that allow for type-safe persistence in Haskell?
Again referring to the state of the Haskell ecosystem, one interesting library is acid-state, which provides ACID guarantees for de/serializing (serializable) Haskell data structures directly, with no external DBMS.
acid-state also uses Template Haskell, and it's relatively straightforward to augment the prior Hello Server data definitions. We define some simple operations we want (query, write), as well as the template invocations:
$(deriveSafeCopy 0 'base ''ServerState)
$(makeAcidic ''ServerState ['writeState, 'queryState])
On the server side, this file has the updated logic to interact with the data store. Since acid-state uses monadic state, we can use ReaderT and again follow the Servant tutorial to thread the data state through the server calls. Although it doesn't seem strictly necessary given the acid-state guarantees, we can use the bracket pattern from Control.Exception to ensure that the data store is closed normally in the event of unexpected server failure.
That's pretty much it! As promised by the stack, generating the Elm code is just a matter of changing some filename configs in the code. In this case, the REST interface didn't change, so changing the backend had no impact on the rest of the client code.
I haven't thought too much yet about the implications of this approach vs using some typed-SQL library like persistent, which I intend the try next.
Hello Server: Haskell + Elm Across Data Boundaries
I started with the thought that it would be fun to write a small, private social network app in Elm, for family and friends to share. This immediately begs the question: what should the backend be?
Setting aside for the moment the class of problems associated with running infrastructure with real users and data on the internet (e.g. security, availability, load, etc), there is the problem of type consistency and safety.
Maintaining safe, static data typing across data boundaries (say, client and server, or server and persistence layer) seems inherently tricky. Ideally, one wants to define data types (or data structures) somewhere, exactly once, and have some abstractions and / or tools that propagate the type guarantees throughout the stack. That would safe effort, minimize boilerplate, and prevent typ(e)o-related errors!
The Elm ecosystem actually has Lamdera, whose the promise is that the server and persistence layers are abstracted away, leaving the developer to reason only about Elm data structures and what data maintain logically in the front vs back end. That's pretty much exactly what I want in this instance! Sadly, it remains in alpha, and doesn't appear to be open source for the moment.
Browsing through the State of the Haskell ecosystem, there are a variety of options for running servers and handling persistence in Haskell (the most obvious fit with Elm). Some of them look like they need project status updates (like Spock... people want to know the status, as it's been dormant for ~three years!). But of course I'm not the first to encounter this problem... so there are a few promising libraries.
Haskell Server + Elm code gen + no persistence
The first is servant-elm, which wraps the elm-bridge library for use with the Servant webserver.
Following Servant's tutorial and the servant-elm README on Github, we can:
- Define ADTs of our choice in Haskell, with corresponding typed REST APIs for Servant
- auto-generate Elm code for corresponding data, including JSON serializers / deserializers that match the REST API (!)
To actually use the Elm client with localhost, I needed to start a temporary version of Chrome with some web security options disabled to account for CORS (borrowed from here).
Next post... adding persistence.
Monday, November 2, 2020
Parser Combinator for SQL Subset
Some more records from RC... a fellow Recurser and I presented on our toy SQL parser combinator at the Friday 5-minute talks (talk slides).
We actually ended up writing parser combinators from scratch twice during RC (first for CIS194 and again for fp-course). The two had almost identical primitives like satisfy :: (Char -> Bool) -> Parser Char, zeroOrMore, oneOrMore, and building up from there... which makes me wonder if there's just a standard recipe for parser combinators?
For the toy SQL parser, we ended up referencing how Stephen Diehl uses Parsec in Kaleidescope once we defined the syntax. It turns our that Parsec provides a bunch of primitives we previously wrote ourselves, like brackets, parens, commaSep... But I think it would have taken us much longer to figure out how to use the package, were it not for Diehl's examples.
https://github.com/tkuriyama/toydb/blob/master/src/Database/Parser.hs
*Database.Parser> process "Insert Into myTable [1, True, \"Jonathan\"];"
Insert "myTable" [FvInt 1,FvBool True,FvText "Jonathan"]
*Database.Parser> process "Insert Into myTable [1, True, \"Jonathan\"]"
(line 1, column 42):
unexpected end of input
expecting ";"
*Database.Parser> process "Insert myTable [1, True];"
(line 1, column 8):
unexpected "m"
expecting "Into"
12 Weeks of RC
Summary of my 12 weeks at Recurse Center (https://www.recurse.com/).
It was, unequivocally, a fun and intellectually rewarding experience that I'd recommend to folks interested in programming -- at pretty much any stage of life or career. It's free to attend, for the moment virtual, and available in 1, 6, and 12 week formats).
- Organized a daily study group for functional programming (mainly Haskell)
- Attended weekly study groups for systems programming and type theory
- Learned about Elm and wrote some visualizations (sudoku, risk)
- Learned about networking primitives and wrote some buggy toy servers
- Contributed to a friend's Haskell game (networked and concurrent!), soon to be internet-playable
- Spent some time with property-based testing, test coverage, and TCR == (Test && Commit) || Revert
- Procrastinated on writing computer-generated music (small example sonifying the Collatz sequence)
- Started a group project to write a toy database in Haskell (with a toy SQL parser combinator and B-Trees)
- Attended 6-12 technical and non-technical a week (most of them 5 mins); gave five 5-min talks
- Attended a great talk on Turing's '36 paper on computation; presented on Dijkstra's '74 essay on scientific thought
- Wrote a few thousand lines of code -- the below table is an inaccurate though representative summary (using tokei)
===============================================================================
Language Files Lines Code Comments Blanks
===============================================================================
C 8 1169 935 31 203
C Header 1 229 64 122 43
Elm 12 1322 1061 43 218
Haskell 49 5231 2232 2327 672
Org 3 236 155 0 81
Python 9 743 585 23 135
Shell 6 16 13 1 2
Zig 3 56 41 0 15
Zsh 1 26 20 1 5
-------------------------------------------------------------------------------
Jupyter Notebooks 4 0 0 0 0
|- Markdown 4 191 0 134 57
|- Python 4 722 623 21 78
(Total) 913 623 155 135
===============================================================================
Total 96 9941 5729 2703 1509
===============================================================================