-
Notifications
You must be signed in to change notification settings - Fork 25
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
map
for non-sequences
#169
Comments
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 There was a fairly decent attempt utilizing coroutines to introduce something akin to the 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 Ferrum comes with 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 |
Thank you for the very detailed response!
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.
Also a fascinating idea.
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 To combine the concept of So, one could take the path of creating 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 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' } |
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! |
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 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 |
Just interested: What are your thoughts on #138? |
Overview
This looks awesome, and makes me want to bring out the whole FP toolkit to use along with ferrum. I'm looking for thoughts and/or best practices for using ferrum with non-sequence objects, too. I.e. more general functors and monads.
Details
The README and project layout make it abundantly clear that the focus is on improve working with iterators. Also, the introduction video makes jokes (which I thoroughly enjoy) when the word "monad" is brought up that no one should have to worry about that means. Still, I am curious what thoughts there are to supporting the more mathematical basis for some of these traits, or at least using this library with other sources that do assume that.
For example, if I start using ferrum, it's only a matter of time before I will want to use some version of type classes for things like Option/Maybe, Either, Result, etc. All of these things should have a
map
operation.map
is hardcoded as applying a function to a sequence, so it cannot be overridden (overloaded? anyway...) with the implementation for non-sequence functors.I think what I would do is start making a separate local lib with a
Functor
Trait and define anfmap
function for it as the basis for other things.That said, I think that the
Sequence
trait could be refactors to be based on aFunctor
trait. I honestly cannot foresee the fallout of that, but I know it would at least make this library significantly more complex. Not least of all because one can go nuts and add inBiFunctor
andApplicative
and.... ooh boy then we have all of Haskell now wrapped up in this bloated library... But seriously, I would just as quickly be looking forfmap_left
andchain
functions as anything else... 🤓 😋Bonus points for any concrete examples of tying ferrum in with any other fp libraries.
Cheers! And thank you!
The text was updated successfully, but these errors were encountered: