sanctuary-js / sanctuary

:see_no_evil: Refuge from unsafe JavaScript
https://sanctuary.js.org
MIT License
3.04k stars 95 forks source link

Maybe.map not functioning as documented. #328

Closed zfoxdev closed 7 years ago

zfoxdev commented 7 years ago

Maybe.map is documented as follows:

Maybe#map :: Maybe a ~> (a -> b) -> Maybe b

While I am encountering the following functionality:

Maybe#map :: Maybe a ~> (a -> b) -> Just b

For example the following code: S.toMaybe('hello').map(str => undefined) Will result in: Just(undefined) When the following is desired: Nothing

This leads to the following issue with the code bellow:

let str = S.toMaybe('hello').map(str => undefined)
let result = S.fromMaybe('world', str)
TypeError: Type-variable constraint violation

fromMaybe :: a -> Maybe a -> a
             ^          ^
             1          2

1)  "world" :: String

2)  undefined :: Undefined
vendethiel commented 7 years ago

That sounds correct to me. You want a filter-style function.

davidchambers commented 7 years ago

Hello, @zfoxdev. Welcome to Sanctuary!

While I am encountering the following functionality:

Maybe#map :: Maybe a ~> (a -> b) -> Just b

The first thing to recognize is that Maybe is a unary type constructor. Its type is as follows:

Maybe :: Type -> Type

So Maybe is not a type, but Maybe String is a type, as is Maybe a.

Just, on the other hand, is not a type constructor but a data constructor. It's a function for creating values of type Maybe a. Its type is as follows:

Just :: a -> Maybe a

Let's consider the types involved in S.toMaybe('hello').map(str => undefined):

toMaybe :: a? -> Maybe a
toMaybe("hello") :: Maybe String
toMaybe("hello").map :: Maybe String ~> (String -> b) -> Maybe b
toMaybe("hello").map(str => undefined) :: Maybe Undefined

The type of the expression is Maybe Undefined. This type has two members: Nothing() and Just(undefined). It's not a particularly useful type, as we have Boolean if we need a type with two members.

My suggestion is to replace or wrap str => undefined. I assume it represents a third-party function (since a Sanctuary user would not deal with null or undefined by choice). This being the case, I suggest defining a wrapper function of type String -> Maybe Foo rather than String -> Foo? (the ? indicates the possibility of undefined). You could then use chain rather than map.

The fromMaybe type error is to be expected. Sanctuary is preventing you from using an expression which can evaluate to values of different types. The problem lies with Just(undefined) rather than with fromMaybe. Avoid getting into that position and the fromMaybe issue will resolve itself.

Let me know if this is helpful. I'm happy to clarify anything I have not done a good job of explaining. :)

safareli commented 7 years ago

Behaviour of map is correct. I think you want to do something like this you can do this:

// return Nothing instead of undefined
m.chain(str => Nothing)// Nothing

or

// return undefined
const f = str => undefined
// but use toMaybe to convert to maybe
m.chain(a => S.toMaybe(f(a))) //Nothing

What's happening in your case is that, you have value of type Maybe String and it's map takes function from String to b and returns Maybe b

:: Maybe String -> (String -> b) -> Maybe b

b for you is substituted by Nullable a or ?a which means that it is either null/undefined or some value a, and result is Maybe (Nullable a)

:: Maybe String -> (String -> Nullable a) -> Maybe (Nullable a)

so Just(undefined) is valid value.


@davidchambers I was writing answer at the same time :d

davidchambers commented 7 years ago

I was writing answer at the same time

No problem. Two explanations are better than one. :)

zfoxdev commented 7 years ago

@davidchambers I did end up using a chain as a work around. Like this example mock code:

let str = S.toMaybe('hello').chain(str => S.toMaybe(undefined))
let result = S.fromMaybe('world', str) //result is set to 'world' as desired

My understanding however based on the documentation is that this should be done automatically when using .map().

I understand that the real issue lies with the Just(undefined) and more specifically with the following issue:

Just(undefined) :: Just(undefined)
Maybe(undefined) :: Nothing

What I would expect from the .map() operation is the second case. Which would no longer make the S.fromMaybe() statement an issue.

Here is a more detailed example to show the desired usage:

 let obj = {
   foo: 'bar'
 };

//This works
let str = S.toMaybe(obj).map(o => o.foo) // :: Just('bar')
let result = S.fromMaybe('bam', str) // result is correctly set to 'bar'

//This does not
let str2 = S.toMaybe(obj).map(o => o.biz) // :: Just(undefined) when Maybe(undefined) aka Nothing() is desired
let result2 = S.fromMaybe('bam', str2) // results in type error rather than a default of 'bam'
svozza commented 7 years ago

It's not map's responsibility to handle this case, all it does is run a function, any function, on the contents of the container it's applied to. If there is a chance of failure then you need to use a function that can handle that (such as S.get in this case) and use chain instead.

safareli commented 7 years ago

If you are not sure about biz being present in the object or not then you shouldn't use map and instead have function: safeGetBiz = (o) => o.biz == null ? Nothing : Just(o.biz) and use it with chain:

let str = S.toMaybe(obj).map(o => o.foo) // Just('bar')
let result = S.fromMaybe('bam', str) // result is correctly set to 'bar'

// This does as well
let str2 = S.toMaybe(obj).chain(safeGetBiz) // Nothing
let result2 = S.fromMaybe('bam', str2) // results is 'bam'

You can generalize safeGetBiz to

safeGet = (key) => (o)  => o[key] == null ? Nothing : Just(o[key])
safeGetBiz = safeGet('biz')
let str2 = S.toMaybe(obj).chain(safeGetBiz)

Sanctuary already has something like safeGet: get :: Accessible a => TypeRep b -> String -> a -> Maybe b

davidchambers commented 7 years ago

Stefano and Irakli have addressed the crux of your recent comment, @zfoxdev, but I would like to clarify the intended meaning of :: in the Sanctuary documentation.

I understand that the real issue lies with the Just(undefined) and more specifically with the following issue:

Just(undefined) :: Just(undefined)
Maybe(undefined) :: Nothing

It looks as though you're using :: to mean evaluates to, as in 2 + 2 evaluates to 4, but in Hindley–Milner notation :: has a different meaning.

:: means is a member of. We can write ['foo', 'bar', 'baz'] :: Array String as shorthand for ['foo', 'bar', 'baz'] is a member of Array String. This implies that the text to the left of the :: must be a JavaScript expression, and the text to the right of the :: must be a type signature. Put tersely, <value> :: <type>.

Just(undefined) :: Just(undefined)

On the left of the :: you have Just(undefined), a valid expression. Just(undefined), though, is not a type, so cannot appear on the right of the ::. The correct type is Maybe Undefined. This makes sense when one considers the type of Just:

Just :: a -> Maybe a

Just takes a value of some type a and returns a value of type Maybe a. When Just is applied to undefined :: Undefined we replace all occurrences of a in the type signature with Undefined giving Just :: Undefined -> Maybe Undefined.

Maybe(undefined) :: Nothing

I think you mean toMaybe(undefined), which evaluates to Nothing(). The type of Nothing(), though, is Maybe a, so one could write:

toMaybe(undefined) :: Maybe a