Code Monkey home page Code Monkey logo

Comments (5)

koraa avatar koraa commented on August 27, 2024

I would love fmap for js; I would love a proper algebraic data types setup for js even more!

As you have noticed, another good name for ferrum might be haskelllish-js! Besides the name being to cumbersome and the obvious PR advantages, there is another reason why it's not called that: Ferrums soundness goals. This is really one of the main features from rust ferrum tries to introduce: Every feature should be hard to abuse and should be relatively edge case free. You can see this in functions like type() or typename() which explicitly deal with null. Or in trySomething styled function which take a default parameter, making you choose which value to return instead of just returning null or undefined (enabling the None = Symbol(); v = trySomething(param, None); if (v === None) … error handling idiom.

There was a fairly decent attempt utilizing coroutines to introduce something akin to the do … notation from haskell: https://medium.com/flock-community/monads-simplified-with-generators-in-typescript-part-1-33486bf9d887
The approach works, but suffers from soundness issues which I outlined here: https://gist.github.com/koraa/78ee4620a5842ede4c62dd65708326c7

I also have reservations to just introducing fmap; it's existence has to be explained to users (why not unify with map() – this is a valid concern in haskell too where the difference between map and fmap is historic as it is here), it also wouldn't be very complete (you got .then but what about .catch? another trait for that?). Should map() try conversion to iterator first or use fmap() directly? Maybe would represent yet a fourth way of representing null-results (besides the null, undefined and the None-idiom). JS devs would as: "But what is wrong with null". The answer is a lot, but it is hard to explain and confusing to integrate with null/undef. Why introduce Either? JS is duck typed? I see the appeal, most js devs wont.

Ferrum comes with ifdef(val?, (v) => v) and isdef(val?) which are useful for dealing with null/undefined in pipes which substitute for Maybe. We could also provide default(val?, def), defaultWith(val?, () => def) – replacing null with a default value. I am considering then(Promise, fn), reject(Promise, fn), finally(Promise, fn) as curryable functions on promises, as well as resolve(val) as a free-standing promise constructor. Potentially also free standing all() and race() functions.

Note these soundness/clarity issues shouldn't stop you from implementing a functor trait. It is perfectly feasible with ferrum, just hard to communicate…

I am planning however to introduce a foundational sequence trait, probably using coroutines; there are a lot of open questions and I am not even sure it is possible, but I could imagine this might work for some monadic types too…

const Begin = Tuple("Begin", seq);
const Gather = Tuple("Gather", it);
const Provide = Tuple("Provide", val);
const EndOfSequence = Tuple("EndOfSequence");

// Implement for sequence/iterator AND async sequence; extendable for reactive/event streams
const AbstractSequence = new Trait("AbstractSequence");

// Implement methods
const map = abstractGenerator(function*(seq, fn) {
  const it = yield Begin(seq);
  while (true) {
    // How does that even work? 
    const v = yield Gather(it);
    if (v === EndOfSequence) break;
    yield Provide(fn(v));
  }
});


const filter = abstractGenerator(function*(seq, fn) {
  const it = yield Begin(seq);
  while (true) {
    // How does that even work? 
    const v = yield Gather(it);
    if (v === EndOfSequence) break;
    if (fn(v)) yield Provide(it);
  }
});

And some very limited control flow for pipe() would be nice so you could pipe(…, await, …) and pipe(…, ifdef, …).

from ferrum.

ptpaterson avatar ptpaterson commented on August 27, 2024

Thank you for the very detailed response!

Ferrum comes with ifdef(val?, (v) => v) and isdef(val?) which are useful for dealing with null/undefined in pipes which substitute for Maybe.

This is a very good point. I am a fan of composing curried and point free functions. This is nice and lightweight -- handles nicely without the extra baggage of mimicking pattern matching.

I am considering then(Promise, fn), reject(Promise, fn), finally(Promise, fn) as curryable functions on promises

Also a fascinating idea.

... implementing a functor trait. It is perfectly feasible with ferrum, just hard to communicate…

Yes. And the deeper I go down the rabbit hole, I can see how much deeper it is :)

I've been trying a few different things, and I've gotten a bit stuck on the convention that creating a new Trait only provides a single invoke function. Some traits require implementation of multiple functions. To invoke a trait, though, only provides a single function per implementation. This is okay for those Traits that only require one function, but is confusing for those with more. A ferrum Trait, is less like a Rust trait, and more like a more specific contract on a single behavior.

To combine the concept of Sequence's map and a functor's fmap, I don't think one should create a ferrum Functor trait. Each implementation actually needs it's own definition of map/fmap, so the ferrum Trait should maybe be more like a specific FMap trait. This is kinda trivial for just Functor, so consider Bifunctor instead, where bimap, first, and second, etc. functions could all be defined for each implementation.

So, one could take the path of creating BiMap, First, and Second ferrum Traits. This technique is kind of embodied in the stdtrait.js definitions. This does sound counter to the idea of a "Trait", but it makes using this library very straightforward.

On the other hand, I've been looking at how a trait could be implemented by providing a thunk to an object with several function definitions, so that multiple function definitions must be provided at the same time.

Also of note: I love the way implementations can be derived! If there were an fmap/map Trait, than it could be implemented automatically for Sequence (though I have not exhaustively tested it).

// FMap Trait Definition
// Could be called "Mappable" instead? for the hardcore FP uninitiated?

const FMap = new Trait('FMap')
const fmap = curry('fmap', (what, f) => FMap.invoke(what, f))

// Derive FMap for Sequence

FMap.implDerived([Sequence], function* ([iter], seq, f) {
  for (const val of iter(seq)) {
    yield f(val)
  }
})

const sequenceTest = pipe([1, 2, 3, 4], fmap(plus(10)), list)
// [11, 12, 13, 14]

easy enough to implement for a trivial Option type, for example

// Option

const Some = function (value) {
  this.value = value
}
const None = function () {}
const none = new None()

FMap.impl(Some, (what, f) => new Some(f(what.value)))
FMap.implStatic(none, () => none)

const someTest = pipe(new Some(2), fmap(plus(4))
)
// Some { value: 6 }

const noneTest = pipe(none, fmap(plus(4)))
// None { }

For a bifunctor, I tried tried out implementing multiple functions under a single trait. While it does the job, it's not very nice, and I'm sure I am breaking the ability to do other things within ferrum.

// Bifunctor

const Bifunctor = new Trait('Bifunctor')
const bimap = curry('bimap', (what, f, g) => Bifunctor.invoke(what).bimap(f, g))
const first = curry('first', (what, f) => Bifunctor.invoke(what).first(f))
const second = curry('second', (what, g) => Bifunctor.invoke(what).second(g))

// Result

const Ok = function (value) {
  this.value = value
}
const NotOk = function (value) {
  this.value = value
}

FMap.impl(Ok, (what, f) => new Ok(f(what.value)))
Bifunctor.impl(Ok, (what) => ({
  bimap: (f, g) => new Ok(f(what.value)),
  first: (f) => new Ok(f(what.value)),
  second: (g) => new Ok(what.value),
}))

FMap.impl(NotOk, (what, f) => new NotOk(what.value))
Bifunctor.impl(NotOk, (what) => ({
  bimap: (f, g) => new NotOk(g(what.value)),
  first: (f) => new NotOk(what.value),
  second: (g) => new NotOk(g(what.value)),
}))

const resultTester = (x) => pipe(x,
    bimap(plus(5), (e) => `Major ${e}`),
    first(plus(40)),
    second((e) => `${e}, Seriously`)
  )

const okTest = resultTester(new Ok(5))
// Ok { value: 50 }
const notOkTest = resultTester(new NotOk('Error'))
// NotOk { value: 'Major Error, Seriously' }

from ferrum.

ptpaterson avatar ptpaterson commented on August 27, 2024

I'll also say that going into this, I did not realize that Rust does not define functor/monad operations through traits. Where there is overlap, the types just implement similar functions based on convention. Rust comes out okay without a generic fmap.

I am thinking of dropping an concern for implementing traits for functors, et al.. But I still love how traits can be used for all the other use cases!

from ferrum.

koraa avatar koraa commented on August 27, 2024

Fascinating! Thank you for sharing the code!

The idiom you are using – returning some object that implements an interface – is exactly what I would recommend for implementing more complex interfaces. It's the same idiom used by the iterator protocol.

Be careful with trait derivations; they are kind of icky, using a O(n) lookup…not a problem if you just have a small number of derivations though.

Btw, your named tuple implementation suffers from some drawbacks…e.g. will act weirdly if not invoked with new…which is why deferred to some "imaginary" future tuple constructor. If you want tuples now I recommend something like this:

function Ok(value) {
  if (isdef(this) || type(this) !== Ok)
    return new Ok(v);
  Object.assign(this, { value });
}

Ok.protoype[Symbol.iterator] = () => iter([this.value]); // Destructuring

from ferrum.

koraa avatar koraa commented on August 27, 2024

Just interested: What are your thoughts on #138?

from ferrum.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.