Closed gcanti closed 5 years ago
@gcanti That's a huge difference with latest! I wonder what's the diff between going through module import
option.map(option.chain(option.map(some(1), n => n * 2), n => (n > 2 ? some(n) : none)), n => n + 1)
and direct import (saving the module access)
import { map, chain } from '...'
map(chain(map(some(1), n => n * 2), n => (n > 2 ? some(n) : none)), n => n + 1)
?
@sledorze not a notable diff (btw here's the source code)
fluent x 54,585,540 ops/sec ±0.42% (89 runs sampled)
static dictionary x 58,900,842 ops/sec ±0.39% (85 runs sampled)
static dictionary (direct) x 63,431,598 ops/sec ±1.04% (85 runs sampled)
Note how the numbers are different this morning... benchmarks aren't my thing, clearly.
Anyway, 58 (or even 48) millions of operations is still a large number and keep in mind that this is a synchronous computation, with TaskEither
or ReaderTaskEither
(i.e. the presumably most common monad stacks) a difference from v1 looks even less important.
TLDR: IMO we should really focus on correctness and DX rather than performance.
focus on correctness and DX
in this order
I really think that, with the new proposed version, I would generate proxy files in order to help with imports (and also perf) without compromising Correctness. (those would define mapArray or mapOption for instance) I m OK with that solution, maybe it can be part of an external repo if too controversial.
I definite +1 in favour of the classless approach is that it appears to have resolved the out-of-memory issue I have been having: https://github.com/Microsoft/TypeScript/issues/30429
Typescript now doesn't have to do structural equality over the large surface areas of classes like None
, Some
, Left
, Right
etc, and all the types they themselves reference.
Got an ETA for the release?
@leemhenson that's good news! thanks for the feedback.
Got an ETA for the release?
Not yet. It also depends on the community feedback, I could release an alpha version if it helps.
I'm using the v2-lib
branch which is sufficient at the moment.
@leemhenson this is the list of test branches
fp-ts
(branch v2-lib
)io-ts
(branch fp-ts-v2
)monocle-ts
(branch fp-ts-v2
)fp-ts-contrib
(branch fp-ts-v2
)newtype-ts
(branch fp-ts-v2
)io-ts-types
(branch fp-ts-v2
)Example (installing from GitHub): npm i gcanti/io-ts#fp-ts-v2
Awesome, thanks.
For what concerns the ecosystem, I also checked
and upgrading looks straightforward
@gcanti Thank you for pinging me! I've ported circuit-breaker-monad
to v2, the results are here: https://github.com/YBogomolov/circuit-breaker-monad/tree/feature/fp-ts-v2
If you're interested, here's my feedback about the migration process:
fold
/foldL
methods. It is a showstopper for both my personal projects and for my work stuff (we are extensively using foldL
over RemoteData
, Option
, Either
, etc.). Had I overlooked something? How can I fold, for example, an Either<E, A>
into a B
using fp-ts@2?@YBogomolov 2) there's a top level fold
function 3) that's definitely a downside. Something like fluent
may help here but works with hardcoded type classes at the moment, so you can map
, chain
, etc... but doesn't provide specialized functions based on the data type at hand (fold
or getOrElse
for example)
I know about the priorities:
focus on correctness and DX (in that order)
But with 3, I'm still failing to convince myself that its a good move overall.
Hope I'm not too late to the party! I just wanted to chime in about some earlier comments on pipe/compose and the inference improvements that have been made in Typescript (@grossbart wrote about this briefly).
It seems that there have been significant improvements in this area with several important issues being closed in TS (https://github.com/microsoft/TypeScript/pull/30114, https://github.com/microsoft/TypeScript/pull/30193 & https://github.com/Microsoft/TypeScript/pull/30215). There are some open issues relating to overloads (https://github.com/microsoft/TypeScript/issues/26591 & https://github.com/microsoft/TypeScript/issues/29732) but those can be avoided by using generic rest parameters as suggested by ahejlsberg here
There is a nice example of well typed pipe/compose here (haven't tested this code yet personally) which suggests that pipe/compose with good inference is currently possible in TS. Personally, I agree with @joshburgess in that I prefer working with data and standalone functions. This style of FP feels more natural to me than the fluent API style.
I'm torn on the issue of fluent v loose functions. On the one hand, I generally prefer loose functions relatedly. On the other hand using a fluent style allows a limited form of closing over type parameters in a natural way which results in less wonkiness.
@sledorze IMO 3) is far from being a show stopper.
In fp-ts@1 the situation is only slightly better:
Validation
These
Traced
Writer
Const
Array
NonEmptyArray
Record
Set
Map
We could even say that in fp-ts@1 chainable APIs are the exception rather than the rule.
I want a more general solution and fluent
is the best candidate so far.
In fp-ts@2 having a chainable API for These
is simple as
import { getMonad, right, left, these } from '../src/These'
import { fluent } from '../src/fluent'
import { semigroupString } from '../src/Semigroup'
const M = getMonad(semigroupString)
const wrap = fluent({ ...M, ...these }) // done
// const x: number
const x = wrap(right(1))
.map(n => n + 1)
.chain(n => (n > 2 ? right(n + 1) : left('foo')))
.bimap(s => s + 'a', n => n - 1)
.reduce(1, (b, a) => b + a)
or for Array
import { array } from '../src/Array'
import { some, none } from '../src/Option'
const wrap = fluent(array)
// const y: Separated<number[], number[]>
const y = wrap([1, 2, 3])
.filterMap(a => (a > 2 ? some(a + 1) : none))
.chain(a => [a + 1, a - 1])
.partition(a => a < 10)
I have to say these fluent examples are starting to convince me of the new approach 😄
@grossbart and the implementation of fluent
is not even complete, we can add support for Compactable
, Ring
, Field
, Lattice
, Bounded
, Category
, IxMonad
...
In this 3D I mostly talked about the downsides so far because I share your concerns and I'm used to tackle the cons before marketing the pros, but there are a lot of benefits in using a "raw" (i.e. unwrapped) encoding for the data types.
1) serialization / deserialization is a breeze
2) no more need for a Validation
data type, we can reuse Either
and just use a different instance
import { left, right } from '../src/Either'
import { fluent } from '../src/fluent'
import { semigroupString } from '../src/Semigroup'
import { getApplicative } from '../src/Validation'
const wrap = fluent(getApplicative(semigroupString))
console.log(
wrap(right(1))
.apFirst(left('a'))
.apFirst(left('b')).value
) // => left('ab')
3) These
is just Either
with an additional Both
member
export type These<L, A> = Either<L, A> | Both<L, A>
4) no more hacks in sum type definitions like type Either<L, A> = Left<L, A> | Right<L, A>
5) Identity<A>
can be actually used as a transparent effect since is now an alias for A
6) monad tansformers are way better now, please take a look at the implementation of Reader
or State
for example (spoiler: they are just ReaderT[Identity]
and StateT[Identity]
)
7) defining an instance of a new, compound data type is easier:
for example, let's say we want to define a monad instance for ArrayOption
import { array } from '../src/Array'
import { Monad1 } from '../src/Monad'
import { Option } from '../src/Option'
import { getOptionM } from '../src/OptionT'
const T = getOptionM(array)
declare module '../src/HKT' {
interface URI2HKT<A> {
ArrayOption: Array<Option<A>>
}
}
interface ArrayOption<A> extends Array<Option<A>> {}
const monadArrayOption: Monad1<'ArrayOption'> = {
URI: 'ArrayOption',
map: T.map,
of: T.of,
ap: T.ap,
chain: T.chain
}
Done.
Ok but where's my chainable API? Just pass monadArrayOption
to fluent
const wrap = fluent(monadArrayOption)
console.log(wrap([some(1), none, some(2)]).map(n => n + 1).value) // [some(2), none, some(3)]
8) Const<L, A>
is just L
+ a phantom type
9) functions / combinators are more reusable
For example there's a withTimeout combinator in fp-ts-contrib
for Task
, and another one for TaskEither
.
In fp-ts@2 we can just define withTimeout
for Task
since TaskEither<L, A> = Task<Either<L, A>>
And probably other benefits that I don't remember now... I'll make sure to list them here when I notice them
Oh.. and fluent
is also useful if you happen to write software in tagless final style
import { Either, fold } from '../src/Either'
import { fluent } from '../src/fluent'
import { HKT } from '../src/HKT'
import { Monad } from '../src/Monad'
// app effect
interface Effect<M, A> extends HKT<M, Either<string, A>> {}
// capabilities
interface MonadStorage<M, V> extends Monad<M> {
read: (s: string) => Effect<M, V>
write: (s: string, n: V) => Effect<M, void>
}
// a program in TF
function program<M>(M: MonadStorage<M, number>): Effect<M, void> {
// yay! I can use chainable APIs here!
const wrapM = fluent(M)
return wrapM(M.read('a')).chain(e => fold(e, () => M.write('a', 1), n => M.write('a', n * 2))).value
}
You can see how 9) (i.e. functions / combinators are more reusable) applies here too. When building an instance of MonadStorage
for Task
import { task } from '../src/Task'
import { fromOption, rightIO } from '../src/TaskEither'
import { lookup } from '../src/Record'
const storage: Record<string, number> = {}
// since TaskEither<L, A> = Task<Either<L, A>> I can reuse TaskEither's functions
// to build an instance for Task
const monadStorageTask: MonadStorage1<'Task', number> = {
...task,
read: s => fromOption(lookup(s, storage), () => 'not found'),
write: (s, n) =>
rightIO(() => {
storage[s] = n
})
}
we can reuse TaskEither
's functions.
@cdimitroulas Re: pipe/compose. AFAIK fp-ts
's pipe
is already well typed, and with ts@3.4.x
can also be used with polymophic functions.
I prefer working with data and standalone functions. This style of FP feels more natural to me than the fluent API style
In order to support this style of programming I'm thinking to add two generic functions: lift
and flatMap
:
let's say I want to define the following (contrived) function without relying on fluent
import { head } from '../src/Array'
import { fluent } from '../src/fluent'
import { option } from '../src/Option'
const pair = <A>(a: A): [A, A] => [a, a]
// f: <A>(as: A[]) => Option<A>
const f = <A>(as: Array<A>) =>
fluent(option)(head(as))
.map(pair)
.chain(head).value
Now if I naively pipe
those functions, either it doesn't type check or I get a different function
import { pipe } from '../src/function'
// f: <A>(a: A[]) => Option<Option<A>>
const f = pipe(
head,
pair,
head
) // not the same function
As shown here https://dev.to/gcanti/getting-started-with-fp-ts-functor-36ek we can think of map
as lift
function lift<A, B>(f: (a: A) => B): (fa: Option<A>) => Option<B> {
return fa => option.map(fa, f)
}
and as shown here https://dev.to/gcanti/getting-started-with-fp-ts-monad-6k we can think of chain
as flatMap
function flatMap<A, B>(f: (a: A) => Option<B>): (fa: Option<A>) => Option<B> {
return fa => option.chain(fa, f)
}
Now we can compose those three functions and obtain the same result as with fluent
by only using static functions
// f: <A>(a: A[]) => Option<A>
const f = pipe(
head,
lift(pair),
flatMap(head)
)
Those lift
and flatMap
helpers above are too specific (Option
) but if we add two generic functions:
lift
in Functor.ts
flatMap
in Chain.ts
parametrized by a Functor
instance (respectively a Chain
instance), we obtain a generic way to build programs by piping functions
const f = pipe(
head,
lift(option)(pair),
flatMap(option)(head)
)
p.s.
lift
is already defined in fp-ts@1
export function lift<F extends URIS3>(
F: Functor3<F>
): <A, B>(f: (a: A) => B) => <U, L>(fa: Type3<F, U, L, A>) => Type3<F, U, L, B>
export function lift<F extends URIS3, U, L>(
F: Functor3C<F, U, L>
): <A, B>(f: (a: A) => B) => (fa: Type3<F, U, L, A>) => Type3<F, U, L, B>
export function lift<F extends URIS2>(
F: Functor2<F>
): <A, B>(f: (a: A) => B) => <L>(fa: Type2<F, L, A>) => Type2<F, L, B>
export function lift<F extends URIS2, L>(
F: Functor2C<F, L>
): <A, B>(f: (a: A) => B) => (fa: Type2<F, L, A>) => Type2<F, L, B>
export function lift<F extends URIS>(F: Functor1<F>): <A, B>(f: (a: A) => B) => (fa: Type<F, A>) => Type<F, B>
export function lift<F>(F: Functor<F>): <A, B>(f: (a: A) => B) => (fa: HKT<F, A>) => HKT<F, B>
export function lift<F>(F: Functor<F>): <A, B>(f: (a: A) => B) => (fa: HKT<F, A>) => HKT<F, B> {
return f => fa => F.map(fa, f)
}
so we just need to define flatMap
Thanks for your response and detailed examples gcanti. What you are saying makes sense and looks like it would enable this style of programming nicely which is great.
Indeed pipe
from fp-ts is working well already, we have been using this quite a bit without issues since ts 3.4.
Ok, just pushed lift
and flatMap
to v2
(and v2-lib
)
As curiosity pipe
can be implemented via fluent
using the functor instance of Reader
(or its Semigroupoid
instance, it's the same) as in v2 Reader<E, A>
is just an alias of (e: E) => A
.
import { fluent } from '../src/fluent'
import { reader } from '../src/reader'
const len = (s: string) => s.length
const double = (n: number) => n * 2
const gt2 = (n: number) => n > 2
// f: Reader<string, boolean>
const f = fluent(reader)(len)
.compose(double)
.compose(gt2).value
However it soon becomes awkward when working with polymorphic functions, so pipe
is the way to go.
Why name them lift
and flatMap
for these two things versus just sticking to the static land naming convention?
What is the static land naming convention? I was under the impression lift
and chain
(rather than flatMap
) are the standard terms used in JS
@cdimitroulas just map
and chain
, I think.
So my plan is:
1) freeze v1 development
2) release fp-ts@2.0.0-rc.1
3) upgrade the ecosystem (monocle-ts
, io-ts
, etc...)
4) think about how to make easier the migration
Cool. I think I will probably release waveguide@0.4 in the next day or do and since I expected 0.5 to take a little longer I can also roll in the fp-ts@2 dependency at that time.
@gcanti Would you be so kind to tell your plans regarding v1? How long it'll be supported?
@YBogomolov what do you mean? Bug fix? ( this is easy, fp-ts@1 is bug free.. :-P ). Backports from v2?
Backports from v2?
Mostly them, yes. Plus for how long will it be valid to create issues for v1?
I think for easier migration of v1 -> v2 codebases, offering v1-equivalent aliases for the functions lift
and flatMap
might be a good idea:
// from v1 fluent style
myTE
.map(fx)
.chain(fy)
// this is a big cognitive jump
pipe(
lift(taskEither)(fx),
flatMap(taskEither)(fy)
)
// this is less scary
pipe(
map(taskEither)(fx),
chain(taskEither)(fy)
)
If you consider sticking another common fn, e.g. mapLeft
in the middle, it's quite jarring:
pipe(
map(taskEither)(fx),
te => taskEither.mapLeft(te, fz),
chain(taskEither)(fy)
)
It would seem a good idea to offer pipeable versions of all the common flow control fns. For example:
import { URIS3, Type3, URIS2, Type2, HKT, HKT2 } from "fp-ts/lib/HKT";
import { Bifunctor3, Bifunctor2, Bifunctor2C, Bifunctor } from "fp-ts/lib/Bifunctor";
export function mapLeft<F extends URIS3>(
F: Bifunctor3<F>,
): <L, M>(f: (l: L) => M) => <U, A>(fa: Type3<F, U, L, A>) => Type3<F, U, M, A>;
export function mapLeft<F extends URIS2>(
F: Bifunctor2<F>,
): <L, M>(f: (l: L) => M) => <A>(fla: Type2<F, L, A>) => Type2<F, M, A>;
export function mapLeft<F extends URIS2, L>(
F: Bifunctor2C<F, L>,
): <L, M>(f: (l: L) => M) => <A>(fla: Type2<F, L, A>) => Type2<F, M, A>;
export function mapLeft<F>(
F: Bifunctor<F>,
): <L, M>(f: (l: L) => M) => <A>(fla: HKT2<F, L, A>) => HKT2<F, M, A>;
export function mapLeft<F>(
F: Bifunctor<F>,
): <L, M>(f: (l: L) => M) => <A>(fla: HKT2<F, L, A>) => HKT2<F, M, A> {
return f => fla => F.mapLeft(fla, f);
}
Which gives:
pipe(
map(taskEither)(fx),
mapLeft(taskEither)(fz),
chain(taskEither)(fy)
)
Not too bad. Could we also offer specialized exports for the common typeclasses? E.g.
import { map, mapLeft, chain } from "fp-ts/lib/specialised/TaskEither";
// e.g. export const map = unspecialised.map(taskEither);
// e.g. export const mapLeft = unspecialised.mapLeft(taskEither);
// e.g. export const chain = unspecialised.chain(taskEither);
pipe(
map(fx),
mapLeft(fz),
chain(fy)
)
That gets you really close to the fluent interface, which would be great considering this just happened: https://github.com/microsoft/TypeScript/pull/31377
@leemhenson we could ditch lift
and flatMap
in favour of a more complete pipeable.ts
module, along the lines of fluent.ts
, i.e. a module that can be configured with type class instances.
POC
// pipeable.ts
import { Bifunctor2 } from './Bifunctor'
import { Chain2 } from './Chain'
import { Type2, URIS2 } from './HKT'
import { identity } from './function'
export interface PipeableChain2<F extends URIS2> {
readonly map: <L, A, B>(f: (a: A) => B) => (fa: Type2<F, L, A>) => Type2<F, L, B>
readonly chain: <L, A, B>(f: (a: A) => Type2<F, L, B>) => (ma: Type2<F, L, A>) => Type2<F, L, B>
readonly flatten: <L, A>(mma: Type2<F, L, Type2<F, L, A>>) => Type2<F, L, A>
}
export interface PipeableBifunctor2<F extends URIS2> {
readonly bimap: <L, A, M, B>(f: (l: L) => M, g: (a: A) => B) => (fa: Type2<F, L, A>) => Type2<F, M, B>
readonly mapLeft: <L, A, M>(f: (l: L) => M) => (fa: Type2<F, L, A>) => Type2<F, M, A>
}
export function getPipeableChain<F extends URIS2>(F: Chain2<F>): PipeableChain2<F> {
return {
map: f => fa => F.map(fa, f),
chain: f => ma => F.chain(ma, f),
flatten: mma => F.chain(mma, identity)
}
}
export function getPipeableBifunctor<F extends URIS2>(F: Bifunctor2<F>): PipeableBifunctor2<F> {
return {
bimap: (f, g) => fa => F.bimap(fa, f, g),
mapLeft: f => fa => F.mapLeft(fa, f)
}
}
Usage
import { taskEither, TaskEither } from './TaskEither'
import { pipe } from './function'
// static functions à la carte
const { map, chain, mapLeft, bimap } = {
...getPipeableChain(taskEither),
...getPipeableBifunctor(taskEither)
}
declare function readFile(path: string): TaskEither<string, string>
declare function len(s: string): number
declare function gt2(n: number): TaskEither<string, boolean>
declare function double(n: number): number
// f: (a: string) => TaskEither<boolean, number>
const f = pipe(
readFile,
map(len),
chain(gt2),
bimap(s => s.length, b => (b ? 0 : 1)),
map(double),
mapLeft(n => n > 0)
)
Note that, like fluent
, pipeable
will work with any instance (Validation
, Writer
, etc..)
Once they are stable, we can backport fluent
and pipeable
to v1.
So, depending on the version and the preferred style, we can choose among:
1) v1 (methods)
myTE
.map(fx)
.chain(fy)
2) v1 / v2 (instances)
M.chain(M.map(myTE, fx), fy)
3) v1 / v2 (fluent
)
wrap(myTE)
.map(fx)
.chain(fy)
.value
4) v1 / v2 (pipeable
)
pipe(
map(fx),
chain(fy)
)
Neat!
@gcanti Great! :)
@gcanti Awesome!
So, depending on the version and the preferred style, we can choose among:
So we end up with 4 different APIs in fp-ts@1.x
and 3 in fp-ts@2.x
. Do we really need them all? I had a hard time with my team dealing with choosing between methods and instance members and this change is going to complicate things even more. Shouldn't we choose something one, ship it with the core and move all the rest to fp-ts-contrib
when releasing fp-ts@2.0
?
@raveclassic I'm torn on this: on one hand fluent
and pipeable
should go in fp-ts-contrib
, on the other hand.. is fp-ts@2
"usable" without them?
So, folks, what do you think?
Is fluent
viable given https://github.com/microsoft/TypeScript/pull/31377 ? It sounds like it won't be filtering accurately any more. For example, with typescript@3.5.0-dev.20190515
:
import { fluent } from "fp-ts/lib/Fluent";
import { option, Option, some, none } from "fp-ts/lib/Option";
const fO = fluent(option);
fO(none).filterWithIndex(...) // <- filterWithIndex appears in intellisense but should only be visible for typeclasses that have FilterableWithIndex, which Option does not.
Unless I'm misunderstanding the mechanism, this looks fatal for fluent
in it's current form.
@leemhenson it affects the DX, the code won't type check
So, folks, what do you think?
@gcanti as already discussed offline, I wouldn't consider fp-ts@2 "usable" the same as v1 is today with only the "instances" API, especially considering the newbie perspective and how to sell fp-ts and FP in general to existing teams/projects:
M.chain(
M.map(
M.chain(
myTE,
f
),
g
),
h
)
is not readable enough (scattered business logic).
"pipeable
" seems promising, I still have to experiment with it but as of today I'd vote to include this API as part of the core, and fluent
into fp-ts-contrib
(especially considering the recently degraded DX).
Personally, I wouldn't really care where my imports are from (core or contrib), and I'd probably pick what I think works best from case to case. But for someone approaching the API for the first time, having a "suggested" path makes a lot of difference I think.
@giogonzo there's still a missing bit in order to make pipe
usable and make the migration easier.
Let's say we have the following code in v1
declare const myTE: TaskEither<Error, string>
declare function readFile(path: string): TaskEither<Error, string>
declare function writeFile(path: string, content: string): TaskEither<Error, void>
const result = myTE
.chain((path: string) => readFile(path + '.txt'))
.map(content => 'header' + content)
.chain(modifiedContent => writeFile('out.txt', modifiedContent))
Now we can migrate to
import { pipe } from '../src/function'
import { pipeable } from '../src/pipeable'
const { map, chain } = pipeable(taskEither)
const result2 = pipe(
chain((path: string) => readFile(path + '.txt')),
map(content => 'header' + content),
chain(modifiedContent => writeFile('out.txt', modifiedContent))
)(myTE)
There are two awkward things though:
(path: string) => readFile(path + '.txt')
myTE
comes last which is counterintuitiveAs suggested by @grossbart here we can add a pipeOf
function that accepts a value instead of a function as first parameter
export function pipeOf<A, B, C>(a: A, ab: (a: A) => B): B
export function pipeOf<A, B, C>(a: A, ab: (a: A) => B, bc: (b: B) => C): C
export function pipeOf<A, B, C, D>(a: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D): D
// etc...
So this
const result = myTE
.chain((path: string) => readFile(path + '.txt'))
.map(content => 'header' + content)
.chain(modifiedContent => writeFile('out.txt', modifiedContent))
becomes this
const result2 = pipeOf(
myTE,
chain(path => readFile(path + '.txt')),
map(content => 'header' + content),
chain(modifiedContent => writeFile('out.txt', modifiedContent))
)
IMO, for purely functional interface having the value last is better, as it allows for easy currying and partial application afterwards. Look at Ramda.js for example of nice pipeable/composable interface.
@YBogomolov not sure I'm following, pipeOf
is just pipe
followed by a function application.
Indeed we can add apply
instead of pipeOf
export function apply<A, B>(a: A, f: (a: A) => B): B {
return f(a)
}
const result = apply(
myTE,
pipe(
chain(path => readFile(path + '.txt')),
map(content => 'header' + content),
chain(modifiedContent => writeFile('out.txt', modifiedContent))
)
)
@leemhenson we could change some APIs and make them data last, for example Option.fold
:
from
export function fold<A, R>(ma: Option<A>, onNone: () => R, onSome: (a: A) => R): R {
return isNone(ma) ? onNone() : onSome(ma.value)
}
to
export function fold<A, R>(onNone: () => R, onSome: (a: A) => R): (ma: Option<A>) => R {
return ma => isNone(ma) ? onNone() : onSome(ma.value)
}
it seems to be beneficial to both pipeable
and fluent
:
Example
Original code (v1)
some(1)
.map(n => n * 2)
.chain(n => (n > 2 ? some(n) : none))
.map(n => n + 1)
.foldL(() => 'none', a => `some(${a})`)
With pipeable
import { pipeable, pipe, apply } from '../src/pipeable'
import { option, some, none, fold } from '../src/Option'
const O = pipeable(option)
apply(
some(1),
pipe(
O.map(n => n * 2),
O.chain(n => (n > 2 ? some(n) : none)),
O.map(n => n + 1),
o => fold(o, () => 'none', a => `some(${a})`) // <= this could simply be `fold(() => 'none', a => `some(${a})`)`
)
)
With fluent
import { fluent } from '../src/fluent'
const wrap = fluent(option)
wrap(some(1))
.map(n => n * 2)
.chain(n => (n > 2 ? some(n) : none))
.map(n => n + 1)
.apply(o => fold(o, () => 'none', a => `some(${a})`)) // <= this could simply be `fold(() => 'none', a => `some(${a})`)`
o => fold(o, () => 'none', a =>
some(${a})
) // <= this could simply befold(() => 'none', a =>
some(${a}))
but then explicit annotations on the functions passed to onNone
/onSome
would be required, correct?
@giogonzo no, TypeScript is able to infer the correct type there
@gcanti we should make sure that this inference scheme will not be deprecated.
We've been beaten in the past with inference regression / changes and if we adopt that style and it gets deprecated, we would be in trouble.
fp-ts
being part of RWC in typescript tests, we should take the opportunity to trigger the TS team to get some feedback on that matter.
@sledorze the API I'm proposing (i.e. pipeOp
= apply . pipe
, a kind of faked pipe operator) works with old versions too, specifically with typescript@2.4.1+
Also:
apply . pipe
(no need for intermediate functions)Here's a standalone repro (please can anyone confirm?)
export type Option<A> = { _tag: 'None' } | { _tag: 'Some'; value: A }
const none: Option<never> = { _tag: 'None' }
const some = <A>(a: A): Option<A> => ({ _tag: 'Some', value: a })
const map = <A, B>(f: (a: A) => B) => (ma: Option<A>): Option<B> => (ma._tag === 'None' ? none : some(f(ma.value)))
const chain = <A, B>(f: (a: A) => Option<B>) => (ma: Option<A>): Option<B> => (ma._tag === 'None' ? none : f(ma.value))
const fold = <A, R>(onNone: () => R, onSome: (a: A) => R) => (ma: Option<A>): R =>
ma._tag === 'None' ? onNone() : onSome(ma.value)
function pipeOp<A, B>(a: A, ab: (a: A) => B): B
function pipeOp<A, B, C>(a: A, ab: (a: A) => B, bc: (b: B) => C): C
function pipeOp<A, B, C, D>(a: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D): D
function pipeOp<A, B, C, D, E>(a: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D, de: (d: D) => E): E
function pipeOp(a: any, ...fns: Array<Function>): Function {
let r: any = a
for (let i = 0; i < fns.length; i++) {
r = fns[i](r)
}
return r
}
const head = <A>(as: Array<A>): Option<A> => (as.length > 0 ? some(as[0]) : none)
const result = pipeOp(
some(10),
chain(n => (n > 2 ? some(n) : none)),
map(n => [n]),
chain(head), // works with polymorphic functions
fold(() => 'none', a => `some(${a})`) // works with curried functions
)
console.log(result) // some(10)
(*)
pipeOp(
x,
f,
g,
h
)
instead of
apply(
x,
pipe(
f,
g,
h
)
)
I'm moving in a similar direction with a project after discovering neverthrow
and fp-ts
after investigating F# and functional domain modelling, and running into the long chains and indentations in typescript.
Have you considered also Promise support for the pipe? I think for me the hardest was to come up with an API that supports both, async and sync code without doubling up the methods, and still being type-safe. And to be able to insert the Promise anywhere in the chain (start, mid, end).
Ah, but I reckon it is easier in fp-ts
due to Task
.
I've added a .pipe
method to Result
, and to Promise
. As soon as im mixing sync with async, I need to start the pipe from a Promise, other than that it's been pretty good so far.
I also landed at a pipe that can be created out of thin air, instead of having to be chained on top of a Result or Promise.
Very similar to this that was posted in https://github.com/gcanti/fp-ts/issues/823#issuecomment-492240744:
// f: (a: string) => TaskEither<boolean, number>
const f = pipe(
readFile,
map(len),
chain(gt2),
bimap(s => s.length, b => (b ? 0 : 1)),
map(double),
mapLeft(n => n > 0)
)
I implemented versions of tee
(don't care about the output, just pass the input) and tuple
(pass both the input and output on, as a tuple), and then ended up with need to be able to flatten tuples when I needed to pass on the ouput of 3 values, not to end up with [[out1, out2], out3]
etc :)
it helps to make:
pipe(
doX(),
flatMap(x =>
flatMap(doY(x).pipe(
flatMap(y => doZ(y).pipe(
flatMap(z => doWithXYZ(x, y, z)),
))
))
)
)
into:
pipe(
flatMap(doX),
flatMap(tup(doY)),
flatMap(flatTup(doZ)),
map(doWithXYZ),
)
I also like the idea of pipeOp
. Really interesting stuff, will be following this project from now on :)
I was also looking into Generators, to kind of implement something similar as async {
, result {
and asyncResult {
Computation Expressions in F#
But typescripts type system doesn't make that pretty yet :) hopefully the TS changes land soon.
It leads to more imperative code, but sometimes that is useful.
@patroza if you switch to using Task
/TaskEither
then you can avoid worrying about Promises until you need to get your final result
@patroza if you switch to using
Task
/TaskEither
then you can avoid worrying about Promises until you need to get your final result
thanks, will dive into it, had not so much luck so far; is there a sample how to do this in the current released API;
Task/TaskEither
and follow with sync functions especially flatMapping other Either's etc.
@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?