node.jspurescript

How do you understand error messages in Purescript?


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?


Solution

  • The first words in the error message are:

    Could not match type Effect with type Aff

    And then it clarifies a bit:

    while trying to match type Effect Unit with type Aff 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 type Aff 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.