Beginning Purescript: Dropping effect rows

Effect rows in Eff (and Aff) are a quite interesting feature when it comes to effectful code in Purescript. On the one hand, they look pretty useful because you can tell immediately what kind of effects are involved in a specific function but, depending on your level of experience and the size and scope of the app you’re writing, they can be a bit cumbersome to work with.

Consider this function

randomNumber :: forall eff. Int -> Int -> Eff ( random :: RANDOM | eff ) Int

The effect row would be ( random :: RANDOM | eff ).

This type signature is saying that the function will make use of the RANDOM effect and that it will work in any effectful program that is at least capable of handling it. If you’re familiar with how records work in Purescript (read this document for a refresher), then rows will look familiar.

In the previous example, the row is said to be open. Now consider this program:

program :: Eff ( random :: RANDOM, console :: CONSOLE ) Unit
program = do
	n <- randomNumber 0 10
	Console.log $ "Random number: " <> (show n)

The row here is closed. This compiles just fine because all the effects involved in program are correctly tracked in its effect row. An important thing to note is that program will only run in a larger program if it provides exactly the RANDOM and CONSOLE effects. That’s because we’re not leaving the row open as we previously did with randomNumber.

main :: Eff ( random :: RANDOM, console :: CONSOLE, http :: HTTP ) Unit
main = do
	Console.log "Let's generate a random number"
	program

This will not compile. The effect rows of main and program do not match and thus we’re not allowed to call program from main (let alone the opposite).

The effect rows we used so far aren’t any more or less interesting than what you would use with records, so the same rules apply.

type User =
	{ name :: String, age :: Int }

type Person =
	{ name :: String, age :: Int, country :: Country }

greet :: forall r. { name :: String | r } -> String
greet { name } = "Hello " <> name <> "!"

The greet function works with any record that has at least a name property of type String. Again, that’s because we’re leaving the row open, so it works with values of type User and Person seamlessly.

Is it worth it?

You now have a better understanding of how effect rows work and what problem they’re trying to solve. We wouldn’t want to have a function making an http request in a context where we’re only expecting text to be written to the console.

A recent survey asked the Purescript community whether a less restrictive (and less powerful) way of dealing with side effects could be a good idea for the language. We’d basically get the IO a type from Haskell, which is exactly like Eff eff a minus the effect row.

Most people agreed with this, so Purescript 0.12 will drop the effect row from Eff (as of the time of writing, it’s still not clear how).

I think it’s good to be aware effect rows exist. At the same time, it’s probably best hold off employing them if you’re a beginner, if anything because they give you a level of type safety you likely don’t need while playing around or writing small apps.

Just drop the effect row

So how do we get a close approximation of IO with the current version of Purescript? You guessed it, by dropping the effect row! We can use the wildcard pattern _ to keep our signatures nice and tidy and not worry about effect tracking.

Essentially, I would write the randomNumber function from before with this type signature:

randomNumber :: Int -> Int -> Eff _ Int

If we rewrite program and main to follow the same pattern, the code we previously wrote will now compile because any effect row will unify with all the functions involved.

program :: Eff _ Unit
program = do
	n <- randomNumber 0 10
	Console.log $ "Random number: " <> (show n)

main :: Eff _ Unit
main = do
	Console.log "Let's generate a random number"
	program -- this will now compile

Note that when you’re using type synonyms, you cannot drop the row directly.

type RandomNumberEff eff =
	Int -> Int -> Eff eff Int

Wildcard patterns cannot be used in type definitions. But this is easily fixed when defining a function with the RandomNumberEff type.

randomNumber :: Int -> Int -> RandomNumberEff _

Conclusion

In general, effect rows give you a level of type safety that is difficult to achieve in other languages. For example, the library purescript-react makes good use of them — I suggest you take a look at the render function, which is not allowed to mutate the state of the component. This is a big win in terms of type safety.

Purescript 0.12 will introduce IO a so most of this post will not apply anymore. But if you want to get started in Purescript right now and don’t want to worry about effect tracking, you now have a little trick up your sleeve to workaround the current effect system.

You can reach me on Twitter.

Thanks to @giuliocanti and @natefaubion for reviewing this post.