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

does .ap work correctly? #206

Closed
overflowz opened this issue Mar 14, 2019 · 28 comments
Closed

does .ap work correctly? #206

overflowz opened this issue Mar 14, 2019 · 28 comments
Assignees
Milestone

Comments

@overflowz
Copy link

Hi, first of all, I'm new into FP and playing around.

I'm trying to find better alternatives to ramda-fantasy for monads. From all the implementations I've searched, I found monet.js most suitable (and great work btw!)

The problem I have currently. Here's an example I'm using from ramda-fantasy with .ap

const greet = name => `Hello ${name}!`;
Maybe.of(greet).ap(Maybe.of('John'));
// -> Maybe.Just("Hello john!")

But when I'm trying same way in monet.js, all I get is errors saying fn is not a function.

Question is, why it does not work the same way and/or how can I do exact same thing in monet?

Regards.

@overflowz
Copy link
Author

EDIT:

If I have many arguments that needs to be passed (rare case, but still), current implementation of .ap makes it look like hell.

For example, let's take a function like this:

const fn = a => b => c => d => e => f => g => a + b + c + d + e + f + g;

With current implementation, I have to write it like this:

Maybe.of(1)
  .ap(Maybe.of(2)
    .ap(Maybe.of(3)
      .ap(Maybe.of(4)
        .ap(Maybe.of(5)
          .ap(Maybe.of(6)
            .ap(Maybe.of(7)
              .map(fn)))))));

but with ramda-fantasy's version, it's cleaner:

Maybe.of(fn)
  .ap(Maybe.of(1))
  .ap(Maybe.of(2))
  .ap(Maybe.of(3))
  .ap(Maybe.of(4))
  .ap(Maybe.of(5))
  .ap(Maybe.of(6))
  .ap(Maybe.of(7));

@ulfryk
Copy link
Member

ulfryk commented Mar 16, 2019

hi @overflowz

looks like that would be much nicer for your flow.

monet implements fantasy-land specs rather then ramda-fantasy - https://github.com/fantasyland/fantasy-land#apply

But this seems to be very useful in 50% cases, so maybe there's a need to add another ap (ap1 ? 🙃 ) to all applicatives in monet :)

And also I'll start work on ramda-fantasy compliance.

@ulfryk ulfryk added this to the 1.0.0 milestone Mar 16, 2019
@overflowz
Copy link
Author

overflowz commented Mar 16, 2019

Hi!

I think there should be an option to specify which one to use, because if other libraries are also depending on it (e.g. if another monad library expects to call .ap but there will be .ap1) it could cause errors. In other monads library, I'm assuming Fluture for example or something else that might come out.

I also tried to modify the .ap by forking project and play around it but test cases went crazy afterward 😅

Regards.

@ulfryk
Copy link
Member

ulfryk commented Mar 16, 2019

Im not sure if we can allow such preconfiguration in monet (releasing 2 versions is an overkill) that would work with typescript typings.

Ramda Fantasy states that it's compatible with fantasy-land but it's not in this case. Maybe there is need to tak one of these actions:

  • change fantasy land specs
  • explicitly state that ramda fantasy is not compatible
  • fix libs that say that are ramda fantasy compliant so they are (and maybe introduce 2 versions of ap)

Another issue is that we'll not be able to define proper typings for the reversed ap version :(

Maybe there is need to introduce .pipe(…) like protocol (like in RxJS) to allow such typings.

…or maybe we can add a bunch of static operators:

const add = Some(a => b => a + b)
const add1 = Applicative.ap(add, Some(1))
const sum = Applicative.ap(add1, Some(3))

but it's quite ugly :(

@ulfryk ulfryk self-assigned this Mar 16, 2019
@ulfryk
Copy link
Member

ulfryk commented Mar 16, 2019

here you can find an open issue stating that ramda-fantasy shoud update it's applicative to be compliant with fantasy-land:

ramda/ramda-fantasy#144

@overflowz
Copy link
Author

Yeah, they're ugly. As far as I know those static functions are available in sanctuary library and without being able to chain monadic operations seems not comfortable for me.

About ordering that is reversed, I didn't knew that though. When I started learning FP in JS I used ramda with ramda-fantasy.

Not sure what will be good solution for this tho :/

@overflowz
Copy link
Author

overflowz commented Mar 22, 2019

I was wondering, what if we check if e.g. Maybe's value type is a function and if so then we other.map(this.value) instead of fn?

@overflowz
Copy link
Author

overflowz commented Mar 22, 2019

e.g. implementation for .ap in Maybe:

ap: function (other) {
            var value = this.val
            return typeof value === 'function'
                ? other.map(value)
                : this.isValue ? other.map(function (fn) {
                    return fn(value)
                }) : this
        }

This still passes all the tests and this works too:

const monet = require('./src/monet');
const Maybe = monet.Maybe;

// logs Just(2)
console.log(
  Maybe.of(x => y => x + y)
    .ap(Maybe.Just(1))
    .ap(Maybe.Just(1))
);

// logs Nothing
console.log(
  Maybe.of(x => y => x + y)
    .ap(Maybe.Nothing())
    .ap(Maybe.Just(1))
);

EDIT: Might be a terrible approach, pardon me. I'm still wrapping my head around FP world.

@iLikeKoffee
Copy link
Collaborator

Sorry for creating another issue. I've got some thoughts about that: #232

@ulfryk
Copy link
Member

ulfryk commented Oct 15, 2020

@iLikeKoffee @overflowz

I think that we can create separate method for swapped (haskell style) ap.

But this has to be typesafe - se we need to investigate that there is a way to express the value check in TypeScript :)

@overflowz
Copy link
Author

just a quick n dirty example to give an idea, wouldn't this work? (I'm aware that I'm using anys there)

class Maybe<T> {
    private constructor(
        private readonly value: T,
    ) { }

    static of<T>(x: T): Maybe<T> {
        return new this(x);
    }

    map<R>(fn: (value: T) => R): Maybe<R> {
        return Maybe.of(fn(this.value));
    }

    ap1(value: Maybe<T extends (arg: infer T) => any ? T : never>): T extends (...args: any) => infer T ? Maybe<T> : never {
        return value.map(this.value as any) as any;
    }
}

@ulfryk
Copy link
Member

ulfryk commented Oct 15, 2020

@overflowz this works for me. Just let's decide if it's ap1 or apTo or apOn or apOver
Any suggestions @iLikeKoffee ?

@ulfryk
Copy link
Member

ulfryk commented Oct 15, 2020

@overflowz just after further investigation I found out that it doesn't proparly handle None cases :(

@overflowz
Copy link
Author

ouch. I'll investigate further when I have a time.

@ulfryk
Copy link
Member

ulfryk commented Oct 15, 2020

class Maybe<T> {
    private constructor(
        private readonly value: T | null,
    ) { }

    static of<T>(x: T): Maybe<T> {
        return new Maybe(x);
    }

    static none<T>(): Maybe<T> {
        return new Maybe<T>(null);
    }

    map<R>(fn: (value: T) => R): Maybe<R> {
        return this.value !== null ? Maybe.of(fn(this.value)) : Maybe.none();
    }

    ap1<V>(value: T extends (arg: infer I) => V ? Maybe<I> : never): T extends (arg: any) => infer R ? Maybe<R> : never {
        const v = this.value;

        if (isNull(v)) {
            return this; // still not good :()
        }

        if (isFn(v)) {
            return value.map(v) as ReturnType<typeof v>;
        }
        throw Error();

    }
}

function isFn(fn: any): fn is ((arg: any) => any) {
    return typeof fn === 'function';
}

function isNull(fn: any): fn is null {
    return fn === null;
}

const a = Maybe.of((a: string) => (b: string) => a + b);

const b = a.ap1(Maybe.of('asdf')).ap1(Maybe.of('asdf'));

This way it's almost good (see - no any )

@iLikeKoffee
Copy link
Collaborator

@ulfryk, ap1 - seems not to be intuitive and self-documenting name.
I'm not native English speaker, but as far as i know functions are applied TO (stack exchange) also, in learn you a haskell for a great good, apply to preposition is used. So, I think that apTo is a the best one.

Also, As far as i know, ap is not fully typesafe and does runtime checks. PoC of Applicative typings.

import { Functor } from 'monet';

interface Applicative<T> extends Functor<T>/* Every applicative is a functor? Shouldn't it inherit Functor interface? */ {
    ap<V> (afn: Applicative<(val: T) => V>): Applicative<V>

    apTo<V> (value: Applicative<V>): T extends (arg: V) => any ? Applicative<ReturnType<T>> : never;

    'fantasy-land/ap'?<V> (afn: Applicative<(val: T) => V>): Applicative<V>
}

type x = { foo: string };
type y = { bar: string };
declare const a: Applicative<(v: x) => y>;
declare const b: Applicative<x>;
declare const c: Applicative<y>;

a.apTo(b).map(value => value.bar); // correct. Value has `y` type

a.apTo(c).map(value => value.bar); // compilation error. `never` type has no `.map` property

@overflowz
Copy link
Author

@ulfryk should it also cover the undefined value (besides null)? also the above looks much cleaner to me (mine was just PoC).

@ulfryk
Copy link
Member

ulfryk commented Oct 15, 2020

@overflowz use what you already have. Actually you will probably not reauire to manually handle null or undefined as map or ap will do it for you. And as you see in @iLikeKoffee example - no need to hande it in term's of types ;)

@iLikeKoffee
Copy link
Collaborator

@overflowz

Maybe.prototype.apTo = function(value){
  return value.ap(this)
} 

apTo - is just inverted ap x.ap(y) is equivalent of y.apTo(x), so we don't need to reimplement something. We have to just reuse ap & provide typings in Applicative interface (and Applicative descendants) .

@jderochervlk
Copy link

What is the status of this being released? I see it's in the development branch. How does development become master and get published? What can I do to help?

@ulfryk
Copy link
Member

ulfryk commented Feb 24, 2021

Ahh yes... I did not have much time lately and looks like GH CI integrations have changed and it stoped working (nightly version should release after each merge to develop branch).

So when Travis integration is fixed we need to create a releas PR - with proper version in package.json, updated CHANGELOG and proper tag (in latest commit). When it's get merged to develop we need to make a PR from develop to master and all the publish stuff should happen automatically.

If you can help with fixing Travis integration @jderochervlk - I can proceed with release :)

( will also add all this info to CONTRIBUTING.md )

@ulfryk ulfryk added the Feature label Mar 2, 2021
@ulfryk ulfryk modified the milestones: 1.0.0, 0.9.2 Mar 2, 2021
@ulfryk
Copy link
Member

ulfryk commented Mar 2, 2021

@iLikeKoffee I did not see any travis scripts triggered by #238 actually, there is probably some change in settings required to make it work. I will try to resolve it this week.

EDIT: but I can see positive outcome - nightly was released :)

A new version of the package monet (0.9.1-460) was published at 2021-03-02T09:26:42.433Z from 104.154.182.187. The shasum of this package was 3e9e6de601dfa10b35870a3f952d3b4a93f6f06b.

@iLikeKoffee
Copy link
Collaborator

@ulfryk
Copy link
Member

ulfryk commented Mar 3, 2021

@iLikeKoffee I can't see the log :(

@ulfryk
Copy link
Member

ulfryk commented Mar 3, 2021

anyway some nightly versions released:

✗ npm info monet versions
 
[
  '0.8.4',             '0.8.5',             '0.8.6',
  '0.8.7',             '0.8.8',             '0.8.9',
  '0.8.10',            '0.9.0-403',         '0.9.0-405',
  '0.9.0-408',         '0.9.0-414',         '0.9.0-417',
  '0.9.0-421',         '0.9.0-422',         '0.9.0-423',
  '0.9.0-426',         '0.9.0-431',         '0.9.0-441',
  '0.9.0-alpha.0',     '0.9.0-alpha.1',     '0.9.0-alpha.2',
  '0.9.0-alpha.3',     '0.9.0-alpha.4',     '0.9.0-alpha.5-0',
  '0.9.0-alpha.5-1',   '0.9.0-alpha.5-2',   '0.9.0-alpha.5-3',
  '0.9.0-alpha.5-353', '0.9.0-alpha.5-356', '0.9.0-alpha.5-4',
  '0.9.0-rc.0',        '0.9.0-rc.1',        '0.9.0-rc.2',
  '0.9.0-rc.3',        '0.9.0-rc.0-358',    '0.9.0-rc.0-361',
  '0.9.0-rc.1-379',    '0.9.0-rc.1-383',    '0.9.0-rc.1-385',
  '0.9.0-rc.1-391',    '0.9.0-rc.2-393',    '0.9.0-rc.3-399',
  '0.9.0',             '0.9.1-444',         '0.9.1-447',
  '0.9.1-460',         '0.9.1-464',         '0.9.1-466',
  '0.9.1-472',         '0.9.1'
]

@iLikeKoffee
Copy link
Collaborator

@ulfryk Are you going to release 0.9.2?

@ulfryk
Copy link
Member

ulfryk commented Mar 4, 2021

@iLikeKoffee yes, just when I clean up few things ( probably during weekend ).

@ulfryk ulfryk closed this as completed Mar 5, 2021
@ulfryk
Copy link
Member

ulfryk commented Mar 5, 2021

A new version of the package monet (0.9.2) was published at 2021-03-05T17:36:17.909Z from
35.227.97.188. The shasum of this package was c59d4390937a1b021126844b50b993963c6b0ec7.

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

No branches or pull requests

4 participants