-
-
Notifications
You must be signed in to change notification settings - Fork 510
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
Start working on v2 #823
Comments
@gcanti Awesome! Sidenote - should we wait for microsoft/TypeScript#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:
|
@grossbart RE: 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)
|
Another thing I've seen is unlawful implementation of instances. |
RE: |
Thanks for the And I'm completely on board with the |
Do we also want to switch to "naked" data types? A glaring example: There's no much of a benefit of using a 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 de
and maybe also
|
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 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. |
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. |
@sledorze IMO the main drivers are the following:
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>
Some data type needs something (provided by the user) in order to implement a particular instance (e.g. Also fluent APIs, even when we can define them, are not enough: see the desire for something like Do in
Serialization / deserialization is just more complicated than necessary.
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 |
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. |
@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:
So for example in the snippet below if we pass
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) |
@joshburgess I'm not sure it's worth it but I'm not against this change (IMO People please vote with a 👍 or a 👎: should we rename |
Can we keep a compatibility layer? (type Alias) |
@gcanti About the 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. |
@gcanti thanks for Some "aha" moments I really liked
Just a question or feature request. But is it possible to make |
@YBogomolov I'm not a fan of default type parameters but what about export function left<E = never, A = never>(e: E): Either<E, A> {
return { _tag: 'Left', left: e }
}
export function right<E = never, A = never>(a: A): Either<E, A> {
return { _tag: 'Right', right: a }
} Result import * as E from '../src/Either'
import { pipe } from '../src/pipeable'
import * as O from '../src/Option'
const ea = E.left<string, number>('e')
const eb = E.right<string, number>(42)
const ec = pipe(
ea,
E.alt(() => eb) // ok
)
const ed = pipe(
eb,
E.alt(() => ea) // ok
)
const a: E.Either<string, number> = pipe(
O.some(42),
O.fold(() => E.left('it was none'), E.right) // ok
)
const res = (cond: boolean) => (cond ? E.left(42) : E.right('what?'))
const v = res(true)
if (E.isRight(v)) {
const x = v.right // string ok
} else {
const x = v.left // number ok
} I get only 2 errors in assert.deepStrictEqual(_.either.ap(_.left('mabError'), _.right('abc')), _.left('mabError')) // Argument of type 'Either<string, (a: never) => never>' is not assignable to parameter of type 'Either<string, (a: string) => unknown>'
const M = _.getValidation(monoidString)
assert.deepStrictEqual(M.ap(_.left('foo'), _.right(1)), _.left('foo')) // Argument of type 'Either<string, (a: never) => never>' is not assignable to parameter of type 'Either<string, (a: number) => unknown>' @mlegenhausen yeah, it's awesome (also
Let me try... stay tuned. |
@gcanti That seems to be really awesome! |
@YBogomolov Also it's more "backward compatible". I guess that it must by applied to the other constructors / lifting functions too // TaskEither.ts
export const left: <E = never, A = never>(e: E) => TaskEither<E, A> = T.left
export const right: <E = never, A = never>(a: A) => TaskEither<E, A> = T.of
export function rightIO<E = never, A = never>(ma: IO<A>): TaskEither<E, A> {
return rightTask(task.fromIO(ma))
}
export function leftIO<E = never, A = never>(me: IO<E>): TaskEither<E, A> {
return leftTask(task.fromIO(me))
}
// etc... Result import * as TE from '../src/TaskEither'
const b = pipe(
O.some(42),
O.fold(() => TE.left('it was none'), a => TE.right(a)) // ok
)
const c = pipe(
O.some(42),
O.fold(() => TE.leftIO(() => 'it was none'), a => TE.rightIO(() => a)) // ok
) |
@mlegenhausen ok it's possible but it would add a lot of code to the @YBogomolov released Also I've published the following versions which are compatible with
|
@gcanti Giulio, thank you! 🙏🏻 In my opinion, such default parameters add a lot to development convenience. I've updated |
To ease without import collision, it would be very handy to NOT have both the module reexport and Type classe implementation having the same name (e.g.: |
@gcanti is there a reason why |
@mlegenhausen export interface Monoidal<F> extends Functor<F> {
readonly unit: () => HKT<F, void>
readonly mult: <A, B>(fa: HKT<F, A>, fb: HKT<F, B>) => HKT<F, [A, B]>
} |
Are there any additional issues before 2.0 release? |
Personally I'm satisfied by the status quo, and I'm ready to release 2.0 #892 |
Thanks to all the last few months have been pretty stressful for me (pushing fp-ts in a new direction and with so many breaking changes is a huge responsability), honestly I didn't expect so much work to do for 2.0 (80 days and counting of almost full time work) but I really do think that the result is great and I could not have done it without your help |
Thanks for all your hard work @gcanti - you're the real OG 🙏 🙌 |
Thank you @gcanti - I haven't contributed much here but I love this library and I am a big fan of all the changes to v2. You did a great job! |
Thanks for all your work, @gcanti the implementations are now a lot easier to follow. This helps a lot learning the FP paradigma. 👏 👏 👏 |
@sledorze @raveclassic @grossbart
I was waiting for microsoft/TypeScript#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?
The text was updated successfully, but these errors were encountered: