5 ms·
Lessons from porting integration tests from Ruby to Haskell
- creichert 12y agoWhile cringing at the cliche: monads were the key ingredient. Let's not turn this into a cliche! Monads are powerful. I find do notation much easier to read in many cases: do subscribe assertSubSucceeded startRoundtripTimer assertRecievedWebHook "channel_occupied" stopRoundtripTimer unsubscribe assertRecievedWebHook "channel_vacated" If you haven’t come across monads before, don’t worry, you can think of the monad we are using as “code that does IO”. On top of this, once you have a basic intuition for how monads work, you start gaining the ability to describe what your program does at the type level. For example, This block of code requires a read-only environment (Reader), has logging output (Writer), and accesses websockets (WebSocketT). Also, the fact that Haskell is a less mainstream language led to a few obvious disadvantages: less documentation, not quite as many libraries, poorer tooling (although having on the fly type errors show up with mod-ghc is already a massive win over Ruby). Yes, you essentially need to be prepared to do any or all of the following if you want to use Haskell commercially: 1) Patch libraries and tooling. 2) Engage communities and maintainers (bug reports, mailing list, etc). 3) Implement functionality (from scratch) which "should be there". If you can't, or won't, do these things, Haskell will be much more painful to use in the real world.
- fractalsea 12y agoAuthor here. Thanks for the feedback! I agree that the do notation does look nice. The on thing that I like about the >> operator is that it's clear that you are performing a combining two functions. But to be honest I don't feel strongly either way. I definitely agree with your point on monads. I tried to keep this post more accessible, but I hope to go into a more technical post in the future on how we used monad transformers when writing these tests. We ended up using WriterT, ReaderT, LoggingT, EitherT for the "test components". Unfortunately the websocket library we used did not play well with this (it is callback based). And I also agree with your final points. Having said that, I had very low expectations in this area going in, and it actually turned out to be not nearly as bad as I expected.
- chowells 12y agoCallbacks are more or less handled by the continuation monad transformer, `ContT`, if you want to use it.
- fractalsea 12y agoInteresting. I think I will have to play around this before I get my head around it, but thanks for the suggestion.
- tel 12y agoAt the risk of being a little pedantic, >> isn't combining two functions, e.g. Just () >> Nothing == Nothing Nothing >> Just () == Nothing It's perhaps viable to think of (m >> f) as modifying/continuing the m effect/computation using a continuation which is constantly the f effect. But that's about as close to "combining two functions" I can get it without specifically picking a monad that is a function. On the other hand, >=> is absolutely combining two functions. It's exactly (flip (.)) in a Kleisli category over some monad.
- fractalsea 12y agoBy "combining" I just meant an operator that takes multiple functions and returns a new function based on those arguments. I guess these "functions" are a special case because they are constant monadic functions. Do you mean to say composition rather than combination when you are about >=>?
- tel 12y agoYeah, I'm happy to abide by this all when the caveats are specialized, but >=> is definitely and directly what's being appealed to. I meant combination just so as to generalize the idea of "composition". If you're willing to call Kleisli composition "composition" generally then I won't complain :)
- innguest 12y agoYes, but let's be careful not to confuse beginners that thought they were on the path to understanding and then you come and say "it's not combining two functions". Well, it is combining two morphisms - that you can't deny. And most people use the shorthand word "function" to mean "any morphism" including type constructors. You may be technically correct but it is not incorrect for people trying to make sense of all these transformations, to say that >> combines two functions. These functions are picked up by the monadic context and executed in sequence.
- eru 12y agoAnd the structure looks a bit like you might want to use a withSubscription function instead of subscribe/unsubscribe pairs. Same for start/stopRoundtripTimer.
- fractalsea 12y agoSomeone mentioned that in a comment on the blog post itself too. It definitely looks like a good pattern to adopt here.
- bos 12y agoI know nothing about the Pusher protocol, but the standard approach to boilerplate encoding/decoding problems these days is to use GHC.Generics. It doesn't always fit (typically when your data structures don't resemble the wire encoding), but it kills all the boilerplate when it does. Since you say that you can magically get away without boilerplate in Ruby in this case, I would expect that the Generics approach will give exactly the same result.
- bos 12y agoIn fact, it turns out the Pusher protocol is all JSON, so you can autogenerate the code. Here's a Pusher message. { "event": "pusher:error", "data": { "message": String, "code": Integer } } Here's the corresponding Haskell. {-# LANGUAGE DeriveGeneric #-} import GHC.Generics -- a generic wrapper type for all Pusher events data Event a = Event { eventType :: Text, eventData :: a } deriving (Generic) instance ToJSON a => ToJSON (Event a) instance FromJSON a => FromJSON (Event a) data Error = Error { message :: Text, code :: Integer } deriving (Generic) instance ToJSON Error instance FromJSON Error
- mikecmpbll 12y agoYou could've also rewritten that dog-ugly Ruby code in Ruby, but an interesting look into Haskell none-the-less.
- fractalsea 12y agoI don't deny that the test could be written in much the same way in Ruby too, the key point of the post was that in Haskell you get flexibility comparable to a highly dynamic language, while also getting a lot safety guarantees. As for the previous tests, they probably a product of many small changes rather than design. There was a reason we were re-writing them.