Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat request: .ap (apply) on ADTs #43

Open
jderochervlk opened this issue Jun 3, 2020 · 9 comments
Open

Feat request: .ap (apply) on ADTs #43

jderochervlk opened this issue Jun 3, 2020 · 9 comments

Comments

@jderochervlk
Copy link

I couldn't find an equivalent function in prelude-ts so please let me know if this exists and I am just missing it.

A common pattern on ADTs (algebraic data types) is ap (short for apply).

From the (Mostly Adequate Guide to Functional Programming)[https://mostly-adequate.gitbooks.io/mostly-adequate-guide/ch10.html#ships-in-bottles]:

ap is a function that can apply the function contents of one functor to the value contents of another.

Examples:
crocks
fp-ts
monet.js

@emmanueltouzery
Copy link
Owner

Hi! Yes, I believe these are applicatives.

so if I look for instance at your crocks link:

// add :: Number -> Number -> Number
const add =
  x => y => x + y

Maybe.of(add)
  .ap(Just(5))
  .ap(Just(27))
//=> Just 32

It seemed to me that the 'lift' approach is easier to handle with typescript (but this can be discussed). So one way to express this same thing in prelude today is like that:

const lifted = Option.liftA2((x:number,y:number) => x+y);
lifted(Option.of(5), Option.of(6));
=> Option.of(11)

const lifted2 = Option.liftA2((x:number,y:number) => x+y);
lifted2(Option.of(5), Option.none<number>());
=> Option.none()

For that specific use-case we achieve the same outcome.

liftA2 is obviously limited to only two parameters. For more, there is liftAp.

const lifted = Option.liftAp((x:{a:number,b:number,c:number}) => x.a+x.b+x.c);
lifted({a:Option.of(5), b:Option.of(6), c:Option.of(3)});
=> Option.of(14)

The lift functions are also available for other functors, like Either and so on.

As per your research, it seems that the ap approach is really seeing more adoption. I'm not sure whether the difference is only ergonomics or whether it also has other advantages. I think at the time I picked the lift approach also because I thought the ap type signature could be messy. This could be revisited potentially.

@jderochervlk
Copy link
Author

jderochervlk commented Jun 3, 2020

I'm not an expert on ADTs, but I am used to seeing things like ap and chain, which seem to be similar to lift but not quite the same.

I found this from some googling:

When "lift" is referred to as a function, though, it's just a variation of map, ap, or chain that takes the function argument first, and returns a new function that takes the functor value and maps/aps/chains on it. That is, given the map-equivalent version of lift, arg.map(f) is identical to lift(f)(arg). Lifting the function, rather than using map directly, makes it easier to pass the function around and apply it to other values, as it lets you use normal function-call syntax, passing arguments, rather than having to specially use the map method.

I'll keep doing some more digging and see if I can do what I want with lift, but it might be good to include alternate ways of doing things, or at least documenting the differences.

@jderochervlk jderochervlk changed the title Feat request: .ap (appy) on ADTs Feat request: .ap (apply) on ADTs Jun 3, 2020
@jderochervlk
Copy link
Author

I think I was able to wrap my head around how to use lift to replace ap, but it has brought something else to my attention.

Consider this workflow (using crocks):

import Maybe from 'crocks/Maybe'

// define two people, one with a defined age and one without
const personOne = Maybe.of({
    name: 'Bob',
})
const personTwo = Maybe.of({
    name: 'Jane',
    age: '33'
})

// this function returns Just the age or Nothing
const getAge = x => 
    x.age ? Maybe.Just(x.age) : Maybe.Nothing()

// get the age and log it, or log that age is not defined
const logAge = x => 
    x.chain(getAge)
     .coalesce(
        () => console.log('age is not defined'),
        console.log
        )

logAge(personOne)
// age is not defined

logAge(personTwo)
// 33

This is a pretty common pattern for me (I use it for rendering a react component if data exists, or rendering an empty state).

Without chain or coalesce I am not sure how to go about creating this flow using prelude-ts.

Perhaps this is a different way of thinking that doesn't line up with prelude-ts.

Since I am expanding the scope of my issue, should I rename or change this issue into something else?

@emmanueltouzery
Copy link
Owner

I think instead of chain you want prelude's flatMap and instead of coalesce you want.. well if you want to return something, you'd use getOrElse or getOrCall. If you just want the side-effect in case the option is not present you'd use ifNone.

The concepts are mostly the same, it's just one style or the other I think.

@jderochervlk
Copy link
Author

Ah, that makes sense.

Here is what I've come up with:

import { Option } from 'prelude-ts'

const { log } = console

const personOne = Option.of({
    name: 'Bob',
})
const personTwo = Option.of({
    name: 'Jane',
    age: '33'
})

const getAge = (x: { age?: number }) => 
    x.age ? Option.some(x.age) : Option.none()

const logAge = (x: Option<any>) => 
    x.flatMap(getAge)
    .ifNone(() => console.log('age is not defined'))
    .map(log)


logAge(personOne)
// age is not defined

logAge(personTwo)
// 33

Is there a language or source for the inspiration of prelude-ts that could help me understand?

@emmanueltouzery
Copy link
Owner

Prelude-ts is inspired by the vavr Java library, itself inspired by the Scala language.

Did you see the prelude user guide?

@jderochervlk
Copy link
Author

jderochervlk commented Jun 3, 2020

I did take a look through that guide. I can try and wrap my head around Scala better, but I'm not sure I'd be able to sell prelude-ts to a team with the current documentation.

I would love to be able to use something like prelude-ts (adts in TypeScript), but I wish it had the level of documentation of ramda or crocks to make it a bit more accessible to people not familiar with Scala.

I'll keep an eye on this library and keep playing around with it, but I am not sure it meets my needs at this point.

@emmanueltouzery
Copy link
Owner

emmanueltouzery commented Jun 3, 2020

No problem! I think it's a matter of which library gives you what you need. What prelude does better than the others, it seems to me, would be on the collections side, especially the proper hashmap with hashing & equality, enabling a proper groupBy for instance, and typescript types (but fp-ts may be even better on that, not sure).

Certainly I would keep this bug open, regarding the ap feature, that's a good point. I'll try to find some time at some point in the future to think how to implement this in a way that the types work out. It may not be possible though.

In the end, I think the differences here are really more cosmetic, these are all functors, applicatives, monads. So i'm really happy to see that ecosystem of solutions!

@jderochervlk
Copy link
Author

I agree about the collections! That is one of the really appealing things to me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants