• Stars
    star
    233
  • Rank 172,230 (Top 4 %)
  • Language
    Haskell
  • Created about 9 years ago
  • Updated almost 7 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

OwnCloud for owls done via The Microservice Architecture

Type-Safe Microservices in Haskell with Servant

10.05.2016 NOTE! The post was updated with the latest servant-0.7 code, but some bugs might be still left. PRs are welcome!

Microservices were becoming a hot thing few years ago and it seems they are still on the rise.

It always surprised me how brave developers are: at one moment they just decide to introduce hundreds contracts of network-separated APIs without having any static proof that it "works well together". I have no idea how others are solving this problem, but was interested in trying it out in Haskell.

This repository is a tutorial (with working code) of super-small service called OwlCloud. It is implemented mostly via Servant framework.

I will try to explain how things look like for those who don't do Haskell every day (I believe most Haskellers could easily do the same anyway).

WARNING! While being small, the service is still almost real-world, contains a lot of features, so it has quite a lot of boilerplate. The purpose is to show that it's not THAT much boilerplate as for a service having all these features.

woop

Architecture overview

                                      +--------------------------------------+   +----------------+
                                 +--->+ Users microservice (/api/users/*)    +---> Users storage  |
                                 |    +-------------+------------------------+   +----------------+
                                 |                  ^
+-----------+      +-------------+                  |
|  Request  +----->+ Front End  ||                  |
+-----------+      +-------------+                  |
                                 |                  |
                                 |    +-------------+------------------------+   +----------------+
                                 +--->+ Albums microservice (/api/albooms/*) +---> Albums storage |
                                      +--------------------------------------+   +----------------+

Request hits a Front-End. Front-end will act as a proxy which only knows prefix of each microservice's public part of API (starts with /api/), and proxies request to it. It won't try to do any other job.

Each microservice is a REST app, which has public (/api/*) and private (/private-api/*) parts. Private parts don't do any security-checks regarding their requestor, as they're assumed to be inside secured network. Additional security could be done in future if needed.

Users microservice will have these end-points:

  • /api/users/owl-in/ -- accept POST-request with a json-document like {"whoo": "user", "passwoord": "who?"}, and return signin token (a string)
  • /api/users/owl-out -- accepts HTTP header Authorization with token to check a user, and signs them out (forgets this token)
  • /private-api/users/token-validity/:token -- returns a validity response if token is still good or not. This is an example of inner API, used by other microservices to not work with database directly, but rather ask this microservice if user's token is valid

Albums microservice will look quite simple:

  • /api/albooms/ -- will check user for a correct Authorization header via Users microservice, and in case of success will render list of albums with photos inside (should I create a microservice for photos to make things more interesting?)

    Should accept sortby query parameter, which is either whoolest (owl for "coolest"), or date.

Installation and Running

If you're curious to play a bit with this repo's code, here are the instructions.

  1. Install haskell stack tool.
  2. Run stack build inside project root (right next to this README)

To run, open 3 terminals and run these commands in them:

stack exec owlcloud-front
stack exec owlcloud-users
stack exec owlcloud-albums

Alternatively, if you have my par tool installed, you can just run:

par "stack exec owlcloud-front" "stack exec owlcloud-users" "stack exec owlcloud-albums"

If stack doesn't work for you, you can try cabal-only-instructions.

Code overview: projects layout

There are 4 cabal projects in root of this project: owlcloud-front, owlcloud-lib, owlcloud-users and owlcloud-albums.

-front will correspond to front-end proxy, -lib contains shared code like routes, types, and common utilities.

-users and -albums are purely microservices.

Routes

In Servant, type-safety of your REST API is taken to the extreme. This means that type expresses not only path of your route, but also types of its dynamic pieces, query-parameters, type of data passed through request-body, expected headers, format and type of return-value. Sounds impressive, isn't it?

But how does it look like, exactly? Well, it's far from looking as an intuitive DSL, but it's good-enough to not want to write one, imho.

The code for routes resides in owlcloud-lib/src/OwlCloud/Types.hs.

Servant API expects you to first describe routes "in types", and then connect them with your routes where you want to. So, you can "implement" your routes several times, just as you can write multiple functions of some type. This lets us describe API for each microservice, and then combine them in bigger API.

type OwlCloudAPI = UsersAPI :<|> AlbumsAPI

This is a central type, which describes all our APIs. We have two type-synonyms for each microservice, and then smash them together with a type-level combinator :<|>. Using type-synonyms means that our type-errors might (and will!) become nasty, and rather suitable for experienced haskeller's brain, but nothing very special to Servant is needed, just a general Haskell type-error-resolving experience.

We don't actually use this type, since our front-end proxy just blindly proxies requests by their prefixes, but having OwlCloudAPI might be useful for documentation and type-checking purposes. You might also combine your microservices in one big service for purposes of testing locally, but it's not in scope of this tutorial.

So, types for Users microservice will look like this:

type UsersAPI =
  "api" :> "users" :> "owl-in" :> ReqBody '[JSON] LoginReq :> Post '[JSON] SigninToken :<|>
  "api" :> "users" :> Authorized ("owl-out" :> Post '[JSON] ()) :<|>
  "private-api" :> "users" :> "token-validity" :> Capture "token" SigninToken :> Get '[JSON] TokenValidity

newtype SigninToken = SigninToken Text
    deriving (ToJSON, FromJSON, FromHttpApiData, ToHttpApiData, Ord, Eq)

data LoginReq = LoginReq
    { whoo      :: Text
    , passwoord :: Text }
    deriving (Generic)

data TokenValidity = TokenValidity
    { isValid :: Bool }
    deriving (Generic, Show)

instance FromJSON LoginReq
instance ToJSON LoginReq
instance FromJSON TokenValidity
instance ToJSON TokenValidity

That's a big piece of code. Let's look closer.

Again, we have individual routes combined together with :<|> at the end of each line.

First route looks like this:

  "api" :> "users" :> "owl-in" :> ReqBody '[JSON] LoginReq :> Post '[JSON] SigninToken

It corresponds (as you might have guessed) to route /api/users/owl-in. ReqBody '[JSON] LoginReq tells that Servant will take a request body, requiring a Content-Type: application/json header in your request, decode it as a JSON decoder (it can support multiple, if you put more in list) into LoginReq type, and pass it as a parameter to your handler, which we'll see later.

Post '[JSON] SigninToken tells us that we'll respond to POST-reuqest, we'll respond in JSON with a SigninToken datatype.

Wow, whole bunch of information about our route, and all that encoded in mostly-readable type-level representation. Neat!

Second route:

  "api" :> "users" :> Authorized ("owl-out" :> Post '[JSON] ())

Everything should be clear except that Authorized function-like thing. What's that? It's a type-synonym I defined at the bottom of the same file:

type Authorized t = Header "Authorization" SigninToken :> t

So, if you replace a type-synonym, your route will look like:

  "api" :> "users" :> Header "Authorization" SigninToken :> "owl-out" :> Post '[JSON] ()

Every resource, which wants to access data of some user, will have to send a SigninToken, which is just a newtype around Text, and put it under Authorization header.

Last route is:

  "private-api" :> "users" :> "token-validity" :> Capture "token" SigninToken :> Get '[JSON] TokenValidity

New part is Capture here. It just tells that we have a dynamic part of a route /private-api/users/token-validity/<dynamic-part-here>, which will be captured and passed as a param into our handler.

Albums microservice types should look quite familiar now:

type AlbumsAPI =
  "api" :> "albooms" :> Authorized (QueryParam "sortby" SortBy :> Get '[JSON] [Album])

data Album = Album [Photo]
    deriving (Generic)

data Photo = Photo
    { description :: Text
    , image       :: URL }
    deriving (Generic)

data SortBy
    = SortByWhoolest
    | SortByDate

instance FromJSON Photo
instance ToJSON Photo
instance FromJSON Album
instance ToJSON Album
instance FromHttpApiData SortBy where
    parseQueryParam "whoolest" = Right SortByWhoolest
    parseQueryParam "date" = Right SortByDate
    parseQueryParam x = Left ("Unknown sortby value:" <> x)
instance ToHttpApiData SortBy where
    toQueryParam SortByWhoolest = "whoolest"
    toQueryParam SortByDate = "date"

We capture a sortby param, which you will pass as ?sortby=date at the end of your url.

Here you can also see manual implementation of FromHttpApiData type-class: it's used to encode/decode url piece value into your datatype.

Handlers

We've covered routes, now we can show how our handlers look like. Let's begin with a Users microservice.

Code resides at ./owlcloud-users/src/Main.hs file.

Let's begin with some machinery to combine individual handlers into UsersAPI type, and then generation of a wai Application type. WAI is a set of contracts, which describe a "reusable haskell web application interface". It's similar to Python's WSGI, if you're familiar with that. After you have a WAI Application, you can run it with a haskell web-server of your choice. We'll use warp, which is as fast as nginx (and sometimes faster!).

server :: Server UsersAPI
server = owlIn :<|> owlOut :<|> tokenValidity

usersAPI :: Proxy UsersAPI
usersAPI = Proxy

app :: Application
app = serve usersAPI server

First thing to notice is that we use a new operator :<|> from Servant, which is a value-level operator (never confuse with :<|>, haha). It combines individual handlers together, and type-system then checks that type of overall expression matches UsersAPI. Errors are somewhat big, as type-synonyms are expanded with not too much help to us, but if you'll look careful enough -- you'll be able to figure thing out.

Now, to individual handlers:

owlIn :: LoginReq -> ExceptT ServantErr IO SigninToken
owlIn LoginReq{..} =
    case (whoo, passwoord) of
      ("great horned owl", "tiger") -> do
          uuid <- liftIO UUID.nextRandom
          let token = SigninToken (UUID.toText uuid)
          liftIO $ atomically $
              modifyTVar db $ \s ->
                s { validTokens = Set.insert token (validTokens s) }
          return token
      _ -> throwE (ServantErr 400 "Username/password pair did not match" "" [])

They start with our /api/users/owl-in handler. We begin with something which amazes me about Servant already: you get your route-parameters as...function parameters!

So, no more silly manual extraction of data from some big Request type: you get what you asked for, and you get it via function parameters. Servant handles the rest for you.

Now, since we don't use a real database for purpose of this tutorial, we'll just allow single login/password pair: ("great horned owl", "tiger"). If it matches, we are generating a SigninToken and put it into a global STM-variable db. It resides inside Common.hs, if you're interested looking at actual implementation, but in real-world app it'll probably just be a database. For curious, type of our database is this:

data State = State
    { validTokens :: Set SigninToken
    , albumsList  :: [Album] }

db :: TVar State
db = unsafePerformIO (unsafeInterleaveIO (newTVarIO (State Set.empty initialAlbums)))

Yes, we use a global variable in Haskell, and sometimes it makes sense, and it's dangerous (as indicated by the scary names).

If you enter wrong credentials, we will respond with a 400-code error, and a help-message describing the reason. You can add some response body, and additional headers if you want to, but I don't.

Error is returned in this interesting way:

throwE (ServantErr 400 "Username/password pair did not match" "" [])

This throwE combinator from transformers package, is something which converts some error-type e into a ExceptT e m a type. The reason we're using it is because Servant uses type ExceptT ServantErr IO a for our handlers. It's a small Monad Transformer stack on top of IO, which allows explicit short-circuiting via ServantErr type, denoting failure.

Our owl-out handler just removes your token from our imaginary database:

owlOut :: Maybe SigninToken -> ExceptT ServantErr IO ()
owlOut mt = do
    checkAuth mt
    maybe (return ()) out mt
  where
    out token = liftIO $ atomically $ modifyTVar db $ \s ->
                  s { validTokens = Set.delete token (validTokens s) }

checkAuth :: Maybe SigninToken -> ExceptT ServantErr IO ()
checkAuth = maybe unauthorized runCheck
  where
    runCheck (SigninToken token) = do
        state <- liftIO $ atomically $ readTVar db
        let isMember = Set.member (SigninToken token) (validTokens state)
        unless isMember unauthorized
    unauthorized =
        throwE (ServantErr 401 "You are not authenticated. Please sign-in" "" [])

Last handler is an inner API to check token validity:

tokenValidity :: SigninToken -> ExceptT ServantErr IO TokenValidity
tokenValidity token = do
    state <- liftIO $ atomically $ readTVar db
    return (TokenValidity (Set.member token (validTokens state)))

Finally, we run our app on port 8082. Of course, this should be stored in some environment variable or config in real-world:

main :: IO ()
main = run 8082 app

We use run from Warp web-server mentioned before.

The Albums microservice shouldn't be much harder to understand. Just one end-point, no need for glueing with :<|> operator:

server :: Manager -> Server AlbumsAPI
server = albums

Also notice that we'll need to pass a Manager value in order to have connection-pooling and caching when we speak to other microservices. It's created in main and just passed in parameters.

Handler:

albums :: Manager -> Maybe SigninToken -> Maybe SortBy -> ExceptT ServantErr IO [Album]
albums mgr mt sortBy = do
    checkValidity mgr mt
    state <- liftIO $ atomically $ readTVar db
    return (albumsList state)

We don't do any actual sorting here, GHC will tell us about this via Warning of unused sortBy variable (how many frameworks tell you your GET-parameters from your API description are not used?).

Now, the interesting part is the checkValidity function. We put it in Common.hs, since it'd be reused by other microservices in the future. It will do a request to the Users microservice, check the validity of a token, and show an error if needed.

checkValidity :: Manager
              -> Maybe SigninToken
              -> ExceptT ServantErr IO ()
checkValidity mgr =
    maybe (throwE (ServantErr 400 "Please, provide an authorization token" "" []))
          (\t -> fly (apiUsersTokenValidity t mgr usersBaseUrl) >>= handleValidity)
  where
    handleValidity (TokenValidity True) = return ()
    handleValidity (TokenValidity False) =
        throwE (ServantErr 400 "Your authorization token is invalid" "" [])

You already understand all the left ... parts which just return errors. But what's fly (apiUsersTokenValidity t), exactly?

Let's make a new sub-header in this tutorial so it's easier to find.

Requesting other microservices

Servant gives you a mechanism to request other services in a type-safe manner. What you need to do, is to "unpack" your type-level API definition into individual request-routes (also in Common.hs):

apiUsersOwlIn :<|> apiUsersOwlOut :<|> apiUsersTokenValidity =
    client (Proxy::Proxy UsersAPI)

apiAlbumsList =
    client (Proxy::Proxy AlbumsAPI)

The scary Proxy::Proxy UsersAPI part is just to move things from type-level to value-level world. This is usually done when you're already able to extract all needed information from just a type, but you need to do some actions with it at the value-level world.

So, we deconstructed some special-built structure (via client function) into individual routines, which are able to request other microservices.

Their types take usual route arguments, and are returning something of type ExceptT ServantError m a. So, these values are not some descriptions, but rather actions themselves, and they do the hard job of requesting microservices for you. Cool!

Notice the ServantError type. It's not the ServantErr type we've seen before, used to short-circuit from handler. Rather, it's a REST-client-response error, which might happen if, say, your microservice is down or responded with an error.

So we implement a special fly function, which will convert the response from one possible error (microservice-request error) into another: the one which we will send to our users, plus some logging.

fly :: (Show b, MonadIO m)
    => ExceptT ServantError m b
    -> ExceptT ServantErr m b
fly apiReq = do
  res <- lift (runExceptT apiReq)
  either logAndFail return res
  where
    logAndFail e = do
        liftIO (putStrLn ("Got internal-api error: " ++ show e))
        throwE internalError
    internalError = ServantErr 500 "CyberInternal MicroServer MicroError" "" []

There you go, now you know how that type-safe microservice-requesting machinery works. Wasn't that hard, wasn't it!

Last bit: front-end

Now, the last bit is to write a front-end. Code is located at owlcloud-front/src/Main.hs file.

I admit, I didn't implement a bullet-proof fully-functional proxy which handles everything in a streaming fashion (sombody, please do so, would be a useful tutorial, and shouldn't take too much code), it's just not what I intend to do in this tutorial, but this one shouldn't be bad in terms of performance.

We define our app as:

app :: Manager -> Application
app mgr req respond =
    case pathInfo req of
        ("api":"users":_) -> microservice "http://localhost:8082/"
        ("api":"albooms":_) -> microservice "http://localhost:8083/"
        _ -> respond (responseLBS status404 [] ",,,(o,o),,,\n ';:`-':;' \n   -\"-\"-   \n")
  where
    microservice = microserviceProxy mgr req respond

We look at request-path, if it begins with /api/users, we micro-forward it to "http://localhost:8082/" (hardcode!). Same for /api/albooms end-point.

We use a wreq package to do actual requests:

microserviceProxy :: Manager -> Request -> (Network.Wai.Response -> IO b) -> Text
                  -> IO b
microserviceProxy mgr req respond basePath = do
    let opts = W.defaults & W.manager .~ Right mgr
                          & W.headers .~ requestHeaders req
                          & W.params .~ getReqParams req
        url = basePath <> T.intercalate "/" (pathInfo req)
    tryProxying opts url `catch` onErr
  where
    tryProxying opts url = do
      r <- case requestMethod req of
             "GET" -> W.getWith opts (toString url)
             "POST" -> requestBody req >>= W.postWith opts (toString url)
      respond (responseLBS (r ^. W.responseStatus) (r ^. W.responseHeaders)
                 (r ^. W.responseBody))
    onErr (StatusCodeException s hdrs _) = respond (responseLBS s hdrs "")
    onErr e = do
      putStrLn ("Internal error: " ++ show e)
      respond (responseLBS status500 [] "Internal server error")

We just re-build a request from the request we received ourselves, and then respond with the response we receive. We implement GET and POST methods only, but you've got the idea for others.

Last bit -- running our front-end:

main :: IO ()
main = do
    mgr <- newManager defaultManagerSettings
    run 8081 (app mgr)

We create a wreq manager, which handles keep-alived connections (to not re-connect to microservice on each request) for us, and just run the web-server.

That's it. That was easy, wasn't it?

Testing

Let us look how it works.

➜  ~  curl -i -XGET -H "Content-Type: application/json" -H "Authorization: badtoken" localhost:8083/api/albooms/
HTTP/1.1 400 Your authorization token is invalid
Transfer-Encoding: chunked
Date: Sat, 12 Sep 2015 09:07:19 GMT
Server: Warp/3.1.3

➜  ~  curl -i -XPOST -H "Content-Type: application/json" --data '{"whoo": "great horned owl", "passwoord": "tiger"}' localhost:8081/api/users/owl-in
HTTP/1.1 201 Created
Transfer-Encoding: chunked
Transfer-Encoding: chunked
Date: Sat, 12 Sep 2015 09:07:36 GMT
Server: Warp/3.1.3
Content-Type: application/json

"88255ebf-2dca-4638-b037-639fb762f6e0"

➜  ~  curl -i -XGET -H "Content-Type: application/json" -H "Authorization: 88255ebf-2dca-4638-b037-639fb762f6e0" localhost:8083/api/albooms/
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Date: Sat, 12 Sep 2015 09:07:48 GMT
Server: Warp/3.1.3
Content-Type: application/json

[[{"image":"http://i.imgur.com/PuhhmQi.jpg","description":"Scating"},{"image":"http://i.imgur.com/v5kqUIM.jpg","description":"Taking shower"}],[{"image":"http://i.imgur.com/3hRAGWJ.png","description":"About to fly"},{"image":"http://i.imgur.com/ArZrhR6.jpg","description":"Selfie"}]]

Conclusion

We just saw how easy it is to write some boilerplate to use The Microservice Architecture, keeping our type-safety for us and our future team mates happy.

I hope you enjoyed it.

Please, send your PRs improving both code and tutorial if you feel like doing that.

LLAP

More Repositories

1

protocol-buffers

Haskell protocol-buffers package
Haskell
78
star
2

par

Small utility that runs multiple computations in parallel
Haskell
40
star
3

boilerpipe

Automatically exported from code.google.com/p/boilerpipe
Java
27
star
4

openrtb-rust

OpenRTB v2.5 and OpenRTB Dynamic Native Ads v1.2 types for rust.
Rust
16
star
5

trigger

Kill and restart the process when the executable changes
Haskell
12
star
6

github-agent

Sync issues to your local directory as a git repo
Haskell
10
star
7

haskell-scripting

Scripting in Haskell
Python
9
star
8

jquery-unparam

A python library to parse jquery.param() string.
Python
7
star
9

fcafe-lazy-evaluation

Talk on 24.11.2017
HTML
4
star
10

mockstar

Mockstar is a small enhance on top of Mock library that gives you declarative way to write your unit-tests.
Python
4
star
11

corenlp-parser

Launches CoreNLP and parses the JSON output
Haskell
3
star
12

htpd

Russian Translation of "How To Design Programs (Second Edition)"
CSS
2
star
13

generic-lens-example

Haskell
2
star
14

pandoc-rebuild

Pandoc gets rebuilt upon every "stack build" command
Haskell
2
star
15

hott-exercises

2
star
16

django-pyfixture

This package lets you write fixtures as regular .py-files.
Python
2
star
17

aeson-migrate

Type-level data migration for aeson values
Haskell
2
star
18

sum-download

Scraping of sum.in.ua
Haskell
2
star
19

bit-protocol

Encode bit protocols not aligned by 8
Haskell
2
star
20

emotive_conjugations

In rhetoric, emotive or emotional conjugation mimics the form of a grammatical conjugation of an irregular verb to illustrate humans’ tendency to describe their own behavior more charitably than the behavior of others.
HTML
2
star
21

dotfiles

My dotfiles
Emacs Lisp
1
star
22

fold-stats

Haskell
1
star
23

haskellbook

Haskell
1
star
24

partial-fill

Type-safe way to fill undefined holes in new data
Haskell
1
star
25

haddock-reexport-bug

Haskell
1
star
26

playground

Random stuff
Haskell
1
star
27

servant-generic-hoist

Trying to migrate app to new servant
Haskell
1
star
28

kyivfprog-2016-folds

HTML
1
star
29

ghcopts-play

Haskell
1
star
30

timeoutbug

Trying to reproduce timeout bug with snap and wreq
Haskell
1
star
31

drinker

1
star
32

wsminimal

Minimal websocket playground
Haskell
1
star
33

th-utils-intero-test

Bug with file paths in intero
Haskell
1
star
34

stack-bug-flag

Repository reproducing bug with passing flags
Haskell
1
star
35

minimal-dotemacs

Minimal dotemacs for linux server-side development
Emacs Lisp
1
star
36

xml-conduit-takewhile1-error

https://github.com/snoyberg/xml/issues/143
Haskell
1
star
37

ghcmod-bug

Bug https://github.com/kazu-yamamoto/ghc-mod/issues/607#issuecomment-146145619
Haskell
1
star
38

noruntests-play

Haskell
1
star
39

redis-tlz

Tools for redis import/export
Haskell
1
star
40

iamkimono

Stupid experiment on parsing
Haskell
1
star
41

nlp

NLP-studying related stuff
HTML
1
star
42

docker-ghc-7.8

Shell
1
star
43

quickcheck-fail

Haskell
1
star
44

play-reflex-semui

Haskell
1
star
45

lambda-calculus-hs

Haskell
1
star
46

warp-keepalive

See https://github.com/yesodweb/wai/issues/707
Haskell
1
star
47

lilypond-play

playground for lilypond
LilyPond
1
star
48

waiwstest

Trying to reproduce all weird errors I had.
Haskell
1
star
49

row-types-default

see https://github.com/target/row-types/issues/42
Haskell
1
star
50

elm-validate-plus

Simple validation upgraded to be applicative-style
Elm
1
star
51

persistent-template-classy

Generate classy lens field accessors for persistent models
Haskell
1
star
52

yulian

tmp
Haskell
1
star
53

docbug

Small doctest bug repro
Haskell
1
star
54

hdcharts

haskell-driven charts
Haskell
1
star
55

dt2unix

Small utility to convert between unix timestamps and readable dates
Haskell
1
star
56

elm-enumerated-list

List with automatic enumeration and indexed removal
Elm
1
star
57

servant-custommonad-logging

See https://github.com/haskell-servant/servant/issues/1180
Haskell
1
star
58

haskell-mode-th-bug

haskell-mode-th-bug
Haskell
1
star
59

random-jungle

Adding disjunction to decisioning trees
Jupyter Notebook
1
star
60

antilorak

Project aimed to improve data from Pantheon
Haskell
1
star
61

hsyslog-udp

Haskell library to log to syslog over a network via UDP
Haskell
1
star
62

circuitt

Fast-circuit monad
Haskell
1
star
63

k-bx.github.io

Personal blog
HTML
1
star
64

web_shelf

Yesod playground (nothing interesting)
Haskell
1
star
65

bottom-up-merge-sort

Bottom-up merge sort from Okasaki
Haskell
1
star
66

dwarf-play

Haskell
1
star
67

phone-quickcheck

Phone exercise from HaskellBook with a quickcheck test
Haskell
1
star
68

waiwarpws

Playing with websockets
Haskell
1
star
69

gogol-play

Haskell
1
star
70

sentiwordnet-parser

Parser for the [SentiWordNet](http://sentiwordnet.isti.cnr.it/) tab-separated file
Haskell
1
star
71

nologesqueleto

Stackoverflow question code https://stackoverflow.com/questions/51674651/nologgingt-does-not-disable-logging-in-persistent/51674652#51674652
Haskell
1
star
72

idris-talk

Idris
1
star
73

playground-gnuradio

Jupyter Notebook
1
star
74

servant-generic-custom-monad

Use servant's generic api together with a custom monad
Haskell
1
star