I'm working on building a crypto arbitrage bot in Purescript, mostly to learn about the functional programming paradigm. As a JS programmer primarily, I find Purescript's error messages very difficult to interpret. Currently, while trying to use the Affjax library to make an http request I'm running into a 'TypesDoNotUnify' error. My code looks like this:
import Affjax.ResponseFormat (ResponseFormat(..))
import Affjax.ResponseFormat as ResponseFormat
import Data.Argonaut.Core (Json, fromString, stringify)
import Data.Either (Either(..))
import Data.HTTP.Method (Method(..))
import Effect (Effect)
import Effect.Aff (Aff, launchAff_)
import Effect.Console (log)
import Node.Express.App (App, listenHttp, get)
import Node.Express.Response (send)
import Node.HTTP (Server)
import Node.HTTP.Client (method)
makeQuoteRequest :: String -> String -> String -> String
makeQuoteRequest fromAddress toAddress amount = "https://api.1inch.exchange/v3.0/137/quote?fromTokenAddress=" <> fromAddress <> "&toTokenAddress=" <> toAddress <> "&amount=" <> amount
sendQuoteRequest :: Either Error Unit
sendQuoteRequest = launchAff_ do
result <- AX.request(AX.defaultRequest {
url = makeQuoteRequest "0xd6df932a45c0f255f85145f286ea0b292b21c90b" "0xc2132d05d31c914a87c6611c10748aeb04b58e8f" "1000000000000000000"
,method = Left GET
,responseFormat = ResponseFormat.json
})
case result of
Left err -> log $ "Quote failed: " <> AX.printError err
Right response -> log $ "GET /api response: " <> stringify response.body
app :: App
app = do
get "/" $ send "<a href='http://localhost:8080/quotes'><button>Get Quotes</button></a>"
get "/quotes" $ send "This is where your quotes should go"
main :: Effect Server
main = do
listenHttp app 8080 \_ ->
log $ "Listening on " <> show 8080
VsCode highlights the line beginning with Left err -> log
as the source of the problem, and when I hover over that the error I get the following additional info:
printError :: Error → String Could not match type
Effect
with type
Aff
while trying to match type Effect Unit with type Aff t0 while checking that expression (apply log) ((append "Quote failed: ") (printError err)) has type Aff t0 in value declaration sendQuoteRequest
where t0 is an unknown type PureScript(TypesDoNotUnify)
What I'm hoping to understand is not only how to fix this error, but to learn a little more about how to interpret the errors that Purescript gives me. What steps would you take to work through this error if it came up in your code?
The first words in the error message are:
Could not match type
Effect
with typeAff
And then it clarifies a bit:
while trying to match type
Effect Unit
with typeAff t0
This is how the vast majority of type errors will look. It means the compiler was following your program along, figuring out which bits have which types, and finally found a bit which from one line of reasoning turned out to have type Effect Unit
, but from another line of reasoning turned out to have type Aff t0
(for some as of yet unknown type t0
). Both lines of reasoning are correct, but their results don't match, so the compiler doesn't know what to do next.
Which were those lines of reasoning you ask? Well, the compiler isn't telling you, and there is a good-ish reason for that: in most real cases it wouldn't make any sense to you anyway. From the compiler's point of view all bits in the line of reasoning are equally important, but there are too many of them to print all at once, and it doesn't know how to pick the ones that would make most sense to you.
But it does give you another valuable piece of information - which bit turned out to have two mismatching types:
while checking that expression
(apply log) ((append "Quote failed: ") (printError err))
has typeAff t0
Here I think the compiler could do a bit of a better job of printing out the expression. It will get there eventually, but for now it just prints with minimal effort. But no fear! We can still decode it.
See that apply
over there? What's that? You didn't write that in your code, so where did it come from?
Well, you kinda did write it in your code: the dollar operator $
is an alias for apply
(see docs). And similarly, the operator <>
is an alias for append
.
Knowing that, we can decode this bit of code into its original form:
(apply log) ((append "Quote failed: ") (printError err))
(($) log) (((<>) "Quote failed: ") (printError err))
($) log ((<>) "Quote failed: " (printError err))
log $ ("Quote failed: " <> (printError err))
log $ "Quote failed: " <> printError err
Hey, look! This is the bit you wrote on line 25!
Ok, so what do we know so far? We know that the compiler has determined that the log $ ...
expression must have the type Effect Unit
on one hand and the type Aff t0
on the other hand.
Let's see what this bit of code is. Look: it's a call to the log
function from Effect.Console
(see docs). Its return type is Effect Unit
. Aha! So that's why the compiler thinks the type should be Effect Unit
! Because it's the result of a call to the log
function!
Ok, good, but what about Aff t0
? Well, let's see where the result of that expression is ultimately going: as a parameter to launchAff_
(see docs). And what type of parameter does launchAff_
take? Surprise - it's Aff a
!
So there you go, mystery solved: launchAff_
expects a parameter of type Aff a
, but you're giving it a value of type Effect Unit
. No wonder the compiler complains!
As for how to fix it.
The first dumb way to do it is to just go and search on Pursuit. We have a value of type Effect Unit
, and we need to convert it to Aff a
. Is that possible? Well, let's search for a function of type Effect Unit -> Aff a
. And behold: there is such function, it's called liftEffect
. So we can just use that:
Left err -> liftEffect $ log $ "Quote failed: " <> AX.printError err
Boom! Done.
But then, if it was me, I would normally think this way: I can't be the first person this happened to. Shouldn't it be a normal occurrence to want to print to console from within an Aff
context? Should't there be already an app for that?
So I might go and search for a suitable type, for example String -> Aff Unit
. And look: there is actually a log
function that fits. It does the same thing, but it works not specifically in Effect
, like the one you're using, but in any monad that has a MonadEffect
instance. Is Aff
such a monad? Well, let's search for Aff
and look: yes, it is. It does have an instance of MonadEffect
.
So now, knowing all of that, I might just change my import from Effect.Console (log)
to Effect.Class.Console (log)
, and then I don't need to make any other changes: the new log
function will just work in Aff
.