How to write an FFI function returning Maybe?

adius [6:27 AM]
How can I implement such a function: foreign import doSomething :: String -> Maybe Number ?

justinw [6:28 AM]
you have to give the constructors to the JS also
(a -> Maybe a) and (Maybe a), Just and Nothing
otherwise you can choose to return foreign and just return either the read result or Nothing on both Nothing and a parse failure

bklaric [6:42 AM]
FWIW what I do is write:

foreign import doSomethingImpl :: String -> Nullable Number
doSomething = doSomethingImpl >>> toMaybe

because dealing with PureScript values like Maybe in JavaScript is not nice.

adius [7:03 AM]

dealing with PureScript type constructors like Maybe in JavaScript is not nice

That’s what I thought :sweat_smile:
Cool, exactly what I was looking for! Thanks @bklaric

bklaric [7:05 AM]
You’re welcome.

gabejohnson [8:55 AM]
@adius, @justinw was saying

foreign import doSomethingImp :: forall a. (a -> Maybe a) -> Maybe a -> String -> Maybe Number

doSomething :: String -> Maybe Number
doSomething = doSomethingImpl Just Nothing

You could also replace a with Number if you wanted.
But toMaybe essentially does ^

1 Like

It’s slightly better to implement it like this:

foreign import doSomethingImp :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> String -> Maybe Number

(moving the foralls into the constructors)
As it reduces the number of possible implementations for the passed-in Just/Nothing such that they’re basically the only allowable arguments.

There used to be a section in the FFI docs about this technique, but I guess it was deleted at some point after transitioning off the wiki?

5 Likes

How does that work? I would have thought that the first argument could be Just or const Nothing and the second argument would have to be Nothing in either case.

Not quite: consider

foreign import lol :: forall a. (a -> Maybe a) -> Maybe a -> Something

which can be called like

lol (Just <<< (_ + 1)) (Just 0)

By putting the forall right at the beginning, you’re saying the caller is allowed to pick any type a, and the implementation has to be able to deal with that (no matter what they pick). If you put it inside the individual arguments it’s the other way around: you’re saying that the implementation is allowed to pick any type a to use those arguments with, and whatever the caller supplies has to work with any possible choice of a.

5 Likes

Yeah, that’s what I was getting at. I perhaps should have provided an example along those lines :slight_smile:

I’m still unclear about the difference between:

foreign import doSomethingImp :: forall a. (a -> Maybe a) -> Maybe a -> String -> Maybe Number

and

foreign import doSomethingImp :: (forall a. a -> Maybe a) -> (forall a. Maybe a) -> String -> Maybe Number

Is the latter equivalent to?:

foreign import doSomethingImp :: forall a b. (a -> Maybe a) -> Maybe b -> String -> Maybe Number

I think this would be easier to understand with some more examples of how the two versions behave differently.


There’s some content on the docs site, but it shows forall a at the beginning:

foreign import doSomethingImpl :: forall a. Fn4 (a -> Maybe a) (Maybe a) (a -> Boolean) a (Maybe a)
doSomethingA
  :: (forall a. a -> Maybe a)
  -> (forall a. Maybe a)
  -> String
  -> Maybe Number

is not the same as

doSomethingB
  :: forall a b
   . (a -> Maybe a)
  -> Maybe b
  -> String
  -> Maybe Number

because the former puts an obligation on the caller to ensure that the first two arguments work with any type a at all, whereas the latter allows the caller to choose what a and b are. For example, I can write

doSomethingB (\x -> Just (x + 1)) (Just "lol")

(which is of course not good if we are expecting Just and Nothing). But trying to call doSomethingA with those arguments won’t work. In fact, there are only two ways of constructing a forall a. a -> Maybe a without resorting to type system escape hatches: Just, and const Nothing. Likewise there’s only one way of constructing a forall a. Maybe a, and that’s Nothing.

3 Likes

I’m curious why the constructors for Just and Nothing couldn’t be imported within the the foreign.js itself instead of having to pass them in as params to doSomething?

They could be, but I wouldn’t recommend that as an approach because it requires you to make more assumptions than providing them on the PS side. It also means the compiler can’t check your work.