Closed gcanti closed 5 years ago
@gcanti We may also reconsider/refresh/restate on that one: https://github.com/gcanti/fp-ts/issues/543
@gcanti Awesome!
Sidenote - should we wait for https://github.com/Microsoft/TypeScript/pull/30790 and try to implement generator-based do-notation once again and add it to the core if succeeded?
@raveclassic is that performant? (sorry not checked by thinking that is not the case)
Some pain points I've seen so far around the HKT encoding (without any proposition):
Maybe we can find some solutions to those, if the solutions result in a breaking change that may be the right time to discuss it
@sledorze I haven't checked performance. Still I think ability to write do-notation-like code is a greater benefit anyway. However current implementation is stack-unsafe.
I like what was listed already π Here's some more (completely random) ideas:
2v
modules (βwhat is this and why is it there?β), so if these will replace the deprecated modules that is a good thing for version 2, I think. (I personally do like this approach to prevent breaking existing code, but I understand the confusion, too.)liftA<n>
where the applied function is curried and has to be manually typed; the way I understand this a simplification along the lines of liftA2(option)((a, b) => {}, optA, optB)
could make type inference for the arguments a
and b
work. (I could be mistaken and it might be that I just couldn't figure out how to make type inference work π.)groupBy
could return a Map
(see also this issue: https://github.com/gcanti/fp-ts/issues/730, for example).ReadonlyArray
s https://github.com/gcanti/fp-ts/pull/544, after all FP is all about not mutating things.readonly
in TS3.4?inspect
functions for Node with the new way to do it https://github.com/gcanti/fp-ts/issues/408@grossbart RE: liftA<n>
. Now that we have sequenceT
/ sequenceS
I think we can deprecate the liftA<n>
functions.
Moreover they don't play well with polymorphic functions
import { liftA2 } from 'fp-ts/lib/Apply'
import { option } from 'fp-ts/lib/Option'
const toObj = <A>(a: A) => <B>(b: B): { a: A; b: B } => ({ a, b })
const toObjLifted = liftA2(option)(toObj)
/*
Note the inferred `{}`
const toObjLifted: <A>(a: Option<A>) => (b: Option<{}>) => Option<{
a: A;
b: {};
}>
*/
I'll send a PR (for v1.17)
liftA2(option)((a, b) => {}, optA, optB)
usually is named map2
, see https://github.com/gcanti/fp-ts/issues/322
Another thing I've seen is unlawful implementation of instances. It may be useful to find a way to enforce lawfulness (or at least ease it / make it the default).
RE: inspect
/ toString
. I propose to skip over the issue by introducing the Show
type class (I'll send a PR)
Thanks for the liftA<n>
explanation: makes a lot of sense! I will try to think of a way to make this findable in the docs β for example, some on our team have struggled with finding Maybe
, not knowing that some languages use Option
for this concept, and this is a similar kind of problem: people will try to find what they know π I didn't know about map<n>
, either.
And I'm completely on board with the Show
typeclass π
Do we also want to switch to "naked" data types?
A glaring example: Writer
.
There's no much of a benefit of using a class
(only map
as a method)
export class Writer<W, A> {
constructor(readonly run: () => [A, W]) {}
map<B>(f: (a: A) => B): Writer<W, B> {
return new Writer(() => {
const [a, w] = this.run()
return [f(a), w]
})
}
}
We could switch to a unwrapped representation
export interface Writer<W, A> {
(): [A, W]
}
We could also declass
ify
Identity
Pair
Tuple
and maybe also
State
Reader
ReaderTaskEither
Note: One of the reasons for preferring instantiating a class over a POJO ( :) ), is the cost of construction (depending on the number of methods/functions).
@sledorze I mean just the function (no POJO)
Here's the Writer
module as an example
import { phantom } from './function'
import { Functor2 } from './Functor'
import { Monad2C } from './Monad'
import { Monoid } from './Monoid'
declare module './HKT' {
interface URI2HKT2<L, A> {
Writer: Writer<L, A>
}
}
export const URI = 'Writer'
export type URI = typeof URI
/**
* @since 1.0.0
*/
export interface Writer<W, A> {
(): [A, W]
}
export const evalWriter = <W, A>(fa: Writer<W, A>): A => {
return fa()[0]
}
export const execWriter = <W, A>(fa: Writer<W, A>): W => {
return fa()[1]
}
const map = <W, A, B>(fa: Writer<W, A>, f: (a: A) => B): Writer<W, B> => {
return () => {
const [a, w] = fa()
return [f(a), w]
}
}
/**
* Appends a value to the accumulator
*
* @since 1.0.0
*/
export const tell = <W>(w: W): Writer<W, void> => {
return () => [undefined, w]
}
/**
* Modifies the result to include the changes to the accumulator
*
* @since 1.3.0
*/
export const listen = <W, A>(fa: Writer<W, A>): Writer<W, [A, W]> => {
return () => {
const [a, w] = fa()
return [[a, w], w]
}
}
/**
* Applies the returned function to the accumulator
*
* @since 1.3.0
*/
export const pass = <W, A>(fa: Writer<W, [A, (w: W) => W]>): Writer<W, A> => {
return () => {
const [[a, f], w] = fa()
return [a, f(w)]
}
}
/**
* Projects a value from modifications made to the accumulator during an action
*
* @since 1.3.0
*/
export const listens = <W, A, B>(fa: Writer<W, A>, f: (w: W) => B): Writer<W, [A, B]> => {
return () => {
const [a, w] = fa()
return [[a, f(w)], w]
}
}
/**
* Modify the final accumulator value by applying a function
*
* @since 1.3.0
*/
export const censor = <W, A>(fa: Writer<W, A>, f: (w: W) => W): Writer<W, A> => {
return () => {
const [a, w] = fa()
return [a, f(w)]
}
}
/**
*
* @since 1.0.0
*/
export const getMonad = <W>(M: Monoid<W>): Monad2C<URI, W> => {
const of = <A>(a: A): Writer<W, A> => {
return () => [a, M.empty]
}
const ap = <A, B>(fab: Writer<W, (a: A) => B>, fa: Writer<W, A>): Writer<W, B> => {
return () => {
const [f, w1] = fab()
const [a, w2] = fa()
return [f(a), M.concat(w1, w2)]
}
}
const chain = <A, B>(fa: Writer<W, A>, f: (a: A) => Writer<W, B>): Writer<W, B> => {
return () => {
const [a, w1] = fa()
const [b, w2] = f(a)()
return [b, M.concat(w1, w2)]
}
}
return {
URI,
_L: phantom,
map,
of,
ap,
chain
}
}
/**
* @since 1.0.0
*/
export const writer: Functor2<URI> = {
URI,
map
}
I've gotten a ton of mileage out of ts-do and would love to see do notation emerge as a priority. It's been really useful in selling FP-TS across various teams where I work.
@cruhl IIRC ts-do
works by patching the prototype
, fp-ts-contrib
's Do looks more lightweight and flexible (works with any Monad
)
What do you think about renaming Setoid to Eq for 2.0? I think you mentioned this before, but not sure.
I want to share that I m a bit worried about discoverability with the new static land approach. It won t ease adoption. What s the main driver? Tree shaking?
@sledorze IMO the main drivers are the following:
1) hacky sum type encoding
Example
type Option<A> = None<A> | Some<A>
instead of
type Option<A> = None | Some<A>
and
type Either<L, A> = Left<L, A> | Right<L, A>
instead of
type Either<L, A> = Left<L> | Right<A>
2) not general
Some data type needs something (provided by the user) in order to implement a particular instance (e.g. Validation
, Writer
, etc...).
For such data types there's no fluent APIs nor a solution at the moment.
Also fluent APIs, even when we can define them, are not enough: see the desire for something like Do in fp-ts-config
3) serialization / deserialization
Serialization / deserialization is just more complicated than necessary.
4) tree shaking
AFAIC classes are a barrier (I'm not an expert though).
However fluent APIs are nice!
fromNullable(foo)
.chain(x => bar)
.map(baz)
versus
map(chain(fromNullable(foo), x => bar), baz)
So fp-ts
users / hackers, I need your help to find a general solution.
Currying for the rescue?
pipe(
chain(x => bar),
map(baz)
)(formNullable(foo))
Or maybe we could create something like
flow(option).chain(x => bar).map(baz).exec(fromNullable(foo))
// or
flow(option)(fromNullable(foo)).chain(x => bar).map(baz)
@mlegenhausen I really like what you're proposing as a fluent API enabler pattern. This is orthogonal. I think I can see how it would be achieved generically via mapped conditionals and I think it could also be possible to tackle userland extension usages that way (but this would includes some limitations when in the 'fluent' form). Do you already have implemented an encoding?
@gcanti thanks for sharing the drivers; that will ease a lot of work (besides the loss of the fluent API).
my bad, wrong button..
@mlegenhausen @sledorze this is a POC based on an idea by @mattiamanzati
Some observations:
1) Fluent
is able to autoconfigure itself (at the type-level) based on the passed instance.
So for example in the snippet below if we pass writer
(from import { writer } from 'fp-ts/lib/Writer
and which is only a Functor
) instead of getMonad(monoidString)
the method chain
is not available (and it doesn't even show up in VS Code).
2) this POC supports Functor
and Monad
but we can add support for other type classes (Setoid
, Semigroup
, Alternative
, Foldable
, etc...)
3) we could also add to Fluent
some functions like flatten
(which could be removed from the Chain
module), apFirst
, apSecond
, chainFirst
, chainSecond
, etc...
import { URIS, Type, HKT, Type2, URIS2 } from 'fp-ts/lib/HKT'
import { Functor1, Functor, Functor2, Functor2C } from 'fp-ts/lib/Functor'
import { Chain1, Chain, Chain2, Chain2C } from 'fp-ts/lib/Chain'
export interface Fluent2C<F extends URIS2, I, L, A> {
readonly I: I
readonly value: Type2<F, L, A>
map<B>(this: Fluent2C<F, Functor2C<F, L>, L, A>, f: (a: A) => B): Fluent2C<F, I, L, B>
chain<B>(
this: Fluent2C<F, Chain2C<F, L>, L, A>,
f: (a: A) => Type2<F, L, B> | Fluent2C<F, I, L, B>
): Fluent2C<F, I, L, B>
}
export interface Fluent2<F extends URIS2, I, L, A> {
readonly I: I
readonly value: Type2<F, L, A>
map<B>(this: Fluent2<F, Functor2<F>, L, A>, f: (a: A) => B): Fluent2<F, I, L, B>
chain<B>(this: Fluent2<F, Chain2<F>, L, A>, f: (a: A) => Type2<F, L, B> | Fluent2<F, I, L, B>): Fluent2<F, I, L, B>
}
export interface Fluent1<F extends URIS, I, A> {
readonly I: I
readonly value: Type<F, A>
map<B>(this: Fluent1<F, Functor1<F>, A>, f: (a: A) => B): Fluent1<F, I, B>
chain<B>(this: Fluent1<F, Chain1<F>, A>, f: (a: A) => Type<F, B> | Fluent1<F, I, B>): Fluent1<F, I, B>
}
const normalize = <F, A>(fa: HKT<F, A> | Fluent<F, unknown, A>): HKT<F, A> => (fa instanceof Fluent ? fa.value : fa)
export class Fluent<F, I, A> {
constructor(readonly I: I, readonly value: HKT<F, A>) {}
map<I extends Functor<F>, B>(this: Fluent<F, I, A>, f: (a: A) => B): Fluent<F, I, B> {
return new Fluent<F, I, B>(this.I, this.I.map(this.value, f))
}
chain<I extends Chain<F>, B>(this: Fluent<F, I, A>, f: (a: A) => HKT<F, B> | Fluent<F, I, B>): Fluent<F, I, B> {
return new Fluent<F, I, B>(this.I, this.I.chain(this.value, a => normalize(f(a))))
}
}
export function fluent<F extends URIS2, I, L>(I: { URI: F; _L: L } & I): <A>(fa: Type2<F, L, A>) => Fluent2C<F, I, L, A>
export function fluent<F extends URIS2, I>(I: { URI: F } & I): <L, A>(fa: Type2<F, L, A>) => Fluent2<F, I, L, A>
export function fluent<F extends URIS, I>(I: { URI: F } & I): <A>(fa: Type<F, A>) => Fluent1<F, I, A>
export function fluent<F, I>(I: { URI: F } & I): <A>(fa: HKT<F, A>) => Fluent<F, I, A>
export function fluent<F, I>(I: { URI: F } & I): <A>(fa: HKT<F, A>) => any {
return fa => new Fluent(I, fa)
}
//
// Usage
//
// Option
import { option, some, none } from 'fp-ts/lib/Option'
const fluentO = fluent(option)
const x = fluentO(some(42))
.map(n => n * 2)
.chain(n => (n > 2 ? some(n) : none)).value
console.log(x) // some(84)
// Writer
import { getMonad, Writer } from 'fp-ts/lib/Writer'
import { monoidString } from 'fp-ts/lib/Monoid'
const fluentW = fluent(getMonad(monoidString))
const y = fluentW(new Writer(() => [1, 'a']))
.map((n: number): number => n + 2)
.chain(n => new Writer(() => [n + 1, 'b'])).value
console.log(y.run()) // [ 4, 'ab' ]
// Either
import { either, right } from 'fp-ts/lib/Either'
const fluentE = fluent(either)
const z = fluentE(right<string, number>(1)).map(n => n + 1).value
console.log(z) // right(2)
What do you think about renaming Setoid to Eq for 2.0?
@joshburgess I'm not sure it's worth it but I'm not against this change (IMO Eq
is nicer than Setoid
).
People please vote with a :+1: or a :-1:: should we rename Setoid
to Eq
?
Can we keep a compatibility layer? (type Alias)
@gcanti About the Fluent
approach, it will also have a runtime impact; I wonder what would be the stance on patching the prototype (can't believe I say that).
But really what I'm not so fan of is the fact one will have to importing modules or be very precise on aliasing specific functions in order to prevent collisions.
So this code
pipe(
chain(x => bar),
map(baz)
)(formNullable(foo))
in RWC will be more like:
pipe(
O.chain(x => bar),
O.map(baz)
)(O.formNullable(foo))
Which also adds an indirection.
There's solutions to that (alias function names prefix defined in the module).
I'm not saying that its good or bad, but we have to be conscious of the tradeoffs. (and once chosen, think about the DX for migrating code bases)
@sleodorze pipe
looks less ergonomic though
const lift = <A, B>(f: (a: A) => B) => (fa: Option<A>): Option<B> => option.map(fa, f)
const flatMap = <A, B>(f: (a: A) => Option<B>) => (fa: Option<A>): Option<B> =>
option.chain(fa, f)
// methods
const x1 = fromNullable(1)
// no type annotation
.chain(n => (n === 0 ? none : some(1 / n)))
.map(n => n * 2)
// fluent
const x2 = fluentO(fromNullable(1))
// no type annotation
.chain(n => (n === 0 ? none : some(1 / n)))
.map(n => n * 2).value
// with pipe
const x3 = pipe(
// type annotation required
flatMap((n: number) => (n === 0 ? none : some(1 / n))),
lift(n => n * 2)
)(fromNullable(1)) // argument comes last
@gcanti Indeed, however, not all API functions are covered by the proposed fluent solution. I wanted to exhibit the import constraints.
I think that pipe and pipeK should be implemented indeed, but in some cases fluent/methods is great because the order of appearance of the things is the same of the order of execution. With pipe you kinda "let me deal with this later" and readability may suffer from this :/
I'm a bit sceptical of the removal of classes because the current fluent API works so smoothly and effortlessly. If we can find a good alternative, I'm all for it, of course, and some examples already point out the possibilities.
One reason for my scepticism stems from a project where we tried the Static Land approach over a Fantasy Land one and it led to very nested and verbose view code and lots of imports. Certainly a matter of taste, practice, and editor support, but a bit of a hurdle none the less.
Another reason arises when we consider this pseudo-code of a React view from a recent project:
const ShowInput = <A>(props: {a: Option<A>, b: Option<A>) => (
<div>
<p>{a.alt(b).map(x => `Input is ${x}`).getOrElse('No input given')}</p>
{b.map(() => <p>[Alternative input]</p>).toNullable()}
</div>
)
With the fluent()
approach I would first have to create the correct instance function and then apply it to both a
and b
, no? Then: would alt
or other combinators be available and when? Also, I'm not sure about getting the value out with fluent(β¦).value
, wouldn't that be a bit confusing: myOpt.isRight() ? fluentO(myOpt).map(β¦).value.value : ''
? I've seen people use this because they didn't know about fold
et al and their editor suggested .value
. I'm not sure this pattern should be encouraged.
In a way a fluent API is the JS way of emulating infix operators, so I went looking for some ideas for how to implement those. Can't be done, of course, but for the curious, I found two ideas: 1) and 2). Not actionable, but maybe leads to other ideas π€·ββοΈ.
Some of the benefits like serializability are certainly worth something and maybe the fluent()
approach works well, so maybe my scepticism is unwarranted π
@joshburgess - What is the motivation behind changing Setoid to Eq? I learned most of this language from Fantasyland, and they use Setoid.
(Not opposed to the change, just curious)
@grossbart this is a direct comparison
(methods)
function ShowInput<A>(props: { a: Option<A>; b: Option<A> }) {
return (
<div>
<p>
{props.a
.alt(props.b)
.map(x => `Input is ${x}`)
.getOrElse('No input given')}
</p>
{props.b.map(() => <p>['Alternative input']</p>).toNullable()}
</div>
)
}
(fluent)
function ShowInputFluent<A>(props: { a: Option<A>; b: Option<A> }) {
return (
<div>
<p>
{getOrElse(
fluent(props.a)
.alt(props.b)
.map(x => `Input is ${x}`).value,
'No input given'
)}
</p>
{toNullable(fluent(props.b).map(() => <p>['Alternative input']</p>).value)}
</div>
)
}
would alt or other combinators be available and when?
We can manage many type classes, see this implementation.
Basically if you pass a suitable instance you can use any of the FluentHKT
/ FluentHKT2
methods.
Actually such a Fluent
wrapper is useful even today for some data types like Validation
, Writer
, etc... so maybe we could release Fluent
as experimental feature in fp-ts-contrib
and try it out in RWC.
Also keep in mind that this is just a first idea (thanks @mattiamanzati !), we could come up with something better in the future.
their editor suggested
.value
. I'm not sure this pattern should be encouraged
This won't happen anymore in v2 as Left
and Right
store the value in different props
@gcanti @grossbart I share the scepticism. I'm worried as much for the people out there which have code base to maintain (this is a short term issue) but also for newcomers, as it adds up to the difficulty to use the lib.
The fluent idea is very neat and the proposed implementation is clever, clean and to the point.
However, as the Fluent interface usage adds some runtime overhead, people will choose between speed and ease of writing, code base styles will become heterogeneous and more verbose, adding to the noise. I'm afraid the DX experience will just become bad.
@mattgrande Ah, I see. I actually didn't know that that's where the Setoid
terminology came from. I think I've seen it somewhere else too... maybe Agda or Idris? I forget.
Anyway, it's not a big deal either way, but I kinda feel like the term unnecessarily scares away newcomers. Both Haskell & PureScript just use Eq
, and Eq
is a bit easier for newcomers in that the word itself resembles what the type class is all about (a function describing the meaning of equals/equality).
I just ran into some initial push-back about the library from team members because of naming like that. But, again, they eventually got over it, and, after that, the name no longer matters. I imagine other teams have had a similar experience.
It's just a matter of taste though, really. Honestly, I'd prefer sticking to terminology like bind
over chain
also, just to be consistent with Haskell/PureScript. I find it difficult when teaching people, because the names for these concepts are often different across libraries/languages... another ex: pure
/point
/of
... but I know FantasyLand/Static Land have already bought into a specific naming convention.
On the fluent API subject: Is this something commonly found in Scala code? I'm assuming we have a number of Scala users weighing in here?
Personally, I'm not a fan of the fluent API. I can deal with it, but it's definitely not my preference. It has a much more OOP-ish feel to it, it bloats the shipped code, it complicates the internal implementations by introducing state in the this
keyword (this really hurts the readability of the implementations, IMO), etc.
I much prefer just working with data & standalone functions. However, the limitations of TypeScript's inference algorithm when trying to work in a functional composition style (compose
/pipe
) do complicate things a bit here. I believe they've improved support for propagating generics recently though? But I'm assuming the right-to-left inference issues are still there...
However, as the Fluent interface usage adds some runtime overhead, people will choose between speed and ease of writing, code base styles will become heterogeneous and more verbose, adding to the noise.
I'm afraid the DX experience will just become bad.
I'm not sure about this. Inference issues aside, quite a lot of people who prefer a FP style were already using libraries like Ramda and lodash/fp which take a data-last & "functions only" (no fluent APIs) approach. Different than native JavaScript, yes, but similar to many of the languages that inspired the libraries.
Stream libraries like most
and rxjs
also migrated to more compositional style APIs.
I am also not sure if it actually introduces some overhead. Ideally the memory footprint should be lower, because of classes being less cheaper to instantiate. But tbh some tests needs to be done in this topic.
Thanks for the examples, @gcanti, the fluent()
one doesn't look as bad as I thought β¦ Still drawn to @sledorze's arguments, a bit π
I tried to research the pipe/compose
inference in TypeScript a bit and haven't found it to be resolved, unfortunately. This solves part of the problem, but not all (cf. this list of issues, haven't read them all).
For the sake of seeing a variety of approaches: this would be a version of pipe
that can infer generic arguments properly. Very inflexible, heretic even, but a middle ground that TypeScript can type just so we can compare it to the other solutions.
// Not a good example because the pipe depends on the data
const inflexiblePipe = <A, B, C>(x: A, f: (x: A) => B, g: (x: B) => C): C => {
return g(f(x));
}
const foo = inflexiblePipe(
fromNullable(1),
// But types can be inferred
flatMap(n => (n === 0 ? none : some(1 / n))),
lift(n => n * 2),
)
@mattiamanzati actually, Fluent is a class.
@sledorze Oh, yeah. I missed that in the calculation. Nevermind, my guess was wrong! xD
But, tbh, if we were looking for performance, tbh the Fluent class can break the immutability rule and just mutate its internal value. It's only a builder afterall, so mutability on Fluent should'nt break anything
@mattiamanzati nothing prevents one to reuse a Fluent value AFAIK, defeating that option for optimisation.
Going back to the proposed new idiomatic fp-ts
way of using combinators; what do we propose for combinators with colliding names.
What's the best DX for that?
One comment per proposal
1) On first usage, lets say of map
, the IDE proposes to import several possibilities.
one choose the option one.
import { map } from 'fp-ts/lib/Option'
so now he wants to use map
for array.
The IDE does not propose anything as there's already a map
in scope.
2) The developper explicitly import the option module
import * as o from 'fp-ts/lib/Option'
And then use it like so o.map
When he wants to use Array's map, he adds
import * as a from 'fp-ts/lib/Array'
etc..
3) The developper explicitly alias the imported map
from Option
import { map as mapOption } from 'fp-ts/lib/Option'
then when he wants the array map
, it does
import { map as mapArray } from 'fp-ts/lib/Array'
4) ( A variation of 3) ) Somewhere the developper has prepared a reusable version of reexported prefixed combinators and can directly invoque the specialised version in its code.
import { mapArray } from 'whatever/Array'
or, if included in fp-ts:
import { mapArray } from 'fp-ts/lib/Array'
the great part is easy discovery and non conflicting imports
Which one would you fp-ts to adopt / people would use / your code base will ends with?
Do you see some other ways?
(please π or π)
@sledorze do we have an option to introduce namespaces for modules? For example Option module:
export type Option<A> = Some<A> | None
export namespace Option {
export const some = <A>(a: A): Option<A> => {
return { _tag: 'Some', value: a }
}
export const map = <A, B>(ma: Option<A>, f: (a: A) => B): Option<B> => {
return isNone(ma) ? ma : some(f(ma.value))
}
}
or Array module:
export namespace Array {
export const map = <A, B>(fa: Array<A>, f: (a: A) => B): Array<B> => {
return fa.map(a => f(a))
}
}
So usage of it would be just:
import { Option } from 'fp-ts/lib/Option'
import { Array } from 'fp-ts/lib/Array'
Option.map(Option.some('foo'), a => a + 'bar') // Some('foobar')
Array.map([1,2,3], x => x * 2) // [2, 4, 6]
@Wenqer we're close to 2. + we gain stable names however I'm not sure about the tradeoffs (I never use namespaces myself).
@mattiamanzati Re: benchmark. FWIW running the following computation
fluent(some(1))
.map(n => n * 2)
.chain(n => (n > 2 ? some(n) : none))
.map(n => n + 1).value
with fp-ts@latest
and the fp-ts#v2
branch I get the following results:
fp-ts@latest
(Option
methods): 201,446,538 ops/secfluent
+ fp-ts#v2
branch: 44,634,930 ops/secfluent
+ fp-ts#v2
branch: 43,813,358 ops/secfp-ts#v2
branch (*): 47,130,759 ops/sec(*)
option.map(option.chain(option.map(some(1), n => n * 2), n => (n > 2 ? some(n) : none)), n => n + 1)
@sledorze @raveclassic @grossbart
I was waiting for https://github.com/Microsoft/TypeScript/pull/26349 in order to start working on
fp-ts@2.x
but alas looks like it will be delayed for an indefinite period.The current codebase has accumulated some cruft during the last few months, which is a good thing because fp-ts is now way better than say a year ago, but is also much confusing for new comers.
I think it's time to clean up the codebase and release a polished
fp-ts@2.x
version:What do you think?