This is a suggestion and perhaps I can help but the documentation for this repo is very hard to read for someone without a background in FP.
I think typescript's evolving type system and JS roots makes it a good cross over language where OO developers can learn functional techniques, myself included. Therefore this library has a tonne of potential to be an introduction.
For example:
I know function binding/chaining is a basic part of FP. I _think_ I have to use the Chain module to do that but I'm not sure and there's no documentation showing that.
The docs that do exist have very little grounding in application settings. Examples which say something like fab -> fba or HKT<A,B> provide little reference for what's actually being attempted. If I don't know what HKT is or what A or B represent then I have little understand of what something does.
I'd love to help improve docs as I learn FP techniques myself. What's the preferred format to contribute? Readmes?
hi piersmacdonald
I totally agree with you. I am a Java programmer and am currently working on a project typeScript where we use fp-ts and io-ts. and I must confess that I have a lot of trouble.
Hi @piersmacdonald, thanks for such great suggestion! I'm totally open for improving the docs and the easiest way to do that is to open some PRs.
Some notes though:
md-files are autogenerated basing on jsdoc in the codeexamples directory is a place where you could put some complex usecases which don't fit into the jsdoc (don't forget to update the section in README.MD)jsdoc for union types (like type Option<A> = Some<A> | None<A>) can be written only for the first type in the union (Some) and that jsdoc is inferred by IDEsI started an effort to make the documentation more accessible to beginners: https://github.com/gcanti/fp-ts/pull/779 It's not where it should be yet (by a far margin), but I plan to work on this over the next weeks. I'll leave this issue open for now until this use case is covered properly 馃槃
I would appreciate more practical usage examples. The documentation at the beginning of Options.ts chapter is good. Number one problem at the moment is figuring out how to import the things you need since there are very few code examples with import statements showing how that works.
I have some background in functional programming, however I'm struggling to deal with objects when using fp-ts. Maybe this is something that the documentation could address. For example I'm not sure what is the idiomatic fp-ts way of transforming...
{
a: Either<Error,A>,
b: Either<Error,B>,
}
...to the following...
Either<Error,{
a: A,
b: b,
}>
My experience from Haskell tells me this is probably somehow related to monads. My experience from Bluebird is telling me that a special function is needed. And my experience from javascript tells me to do Object.entries then do some kind of transformation on the pairs and then do Object.fromEntries to the object back, however that seems quite verbose for such a simple task.
@cyberixae You need a sequenceS parametrized with Either as Apply:
type Foo = {
a: Either<Error, number>,
b: Either<Error, string>
}
declare const foo: Foo
const result = sequenceS(either)(foo)
Some more noob questions.
One more regarding dealing with objects. What is the preferred way of doing a map on an object? I'm still trying to do that with Object.entries, then map, then Object.fromEntries but this creates problems since they introduce undefined into types of the [key, value] pairs for some reason.
Then another one about dealing with Task and IO. By reading the existing documentation I managed to use sequence to combine several tasks and I/O operations. I'm doing computations that only have side effects and I don't really care about the result. However, because of this the type of the IO value gets quite complicated. It ends up being some complex structure with lots of void values. Should I just define the functions as Task<any> and IO<any>? Doing extra computation to convert Task<void[]> to Task<void> and IO<void[]> to IO<void> feels overkill but I'm also a bit worried that using the any type might cause problems.
One more regarding dealing with objects. What is the preferred way of doing a map on an object?
You'll probably want to try map or mapWithIndex (if you need keys too) from fp-ts/lib/Record?
Doing extra computation to convert Task
and IO feels overkill but I'm also a bit worried that using the any type might cause problems.
I'd always avoid resorting to any, as it leaks into the rest of the codebase quite easily.
I'd say void is ok here, and if you find yourself mapping with () => {} to return Task<void> a lot of times you could reuse constVoid from fp-ts/lib/function: .map(constVoid)
I'm doing computations that only have side effects and I don't really care about the result
@cyberixae you can use traverse_ / sequence_ from Foldable2v
import { IO, io } from 'fp-ts/lib/IO'
import { array } from 'fp-ts/lib/Array'
import { sequence_ } from 'fp-ts/lib/Foldable2v'
declare const fu1: IO<void>
declare const fu2: IO<void>
// x: IO<void>
const x = sequence_(io, array)([fu1, fu2])
I think I have understood the basics of working with Task and Either. However, when I'm trying to work with Task, Either and TaskEither I get very easily quite confused.
// I have the following
decode: (a: Request) => Either<Err,Query>
process: (a: Query) => Task<Result>
encode: (a: Either<Err,Result>) => Response
// I'm trying to write
mystery: (a: Request) => Task<Response>
@cyberixae if I properly understand your issue, I think this could be helpful:
Task, Either and other data types (ref here) are abstractions which represent and encode some kind of "effect" (quotes intended);Task represents an async computation with side-effects which never fails;Either represents a computation which may fail;NaN when you're trying to parseInt a string), an exception (like those thrown by JSON.parse) or some bad response from external services (like an API returning 500 status code);Task and Either (Task<Either<L, R>>) you're encoding into the fp-ts lingo an async computation which may fail;Either) you have to do a double map: const mapped = aTask.map(aEither => aEither.map(fn));TaskEither combinator comes to the rescue; it is like Task<Either<L, R>> but flattened: const mapped = aTaskEither.map(fn)Going back to you problem, it seems that you need a computation which:
Request and tries to decode it into a Query, giving an Err if it fails;Query and the value will be a Result or an Err (this is the missing part in you snippet);Result is encoded into a Response.There is a pattern (if...then) into this "flow" which is expressed by Either, but there is also an async operation so you have to lift all the thing into a TaskEither.
The type of your computation will be something like:
declare function mystery(a: Request): TaskEither<Err, Response>;
and you can split it as you prefer (TaskEither is lazy and short-circuited - a.k.a. _fail-fast_):
import {fromEither} from 'fp-ts/lib/TaskEither';
// from you snippet
declare function decode(a: Request): Either<Err, Query>;
declare function process(a: Query): TaskEither<Err, Result>;
declare function encode(a: Result): Response;
function mystery(a: Request): TaskEither<Err, Response> {
return fromEither(decode(a)) // you need to lift an Either into a TaskEither
.chain(process)
.map(encode);
}
If decode fails, mystery fails (left branch of TaskEither<Err, Response>).
If process fails, mistery fails (left branch of TaskEither<Err, Response>).
encode never fails because it can always conver a Result into a Response (right branch if TaskEither<Err, Response>).
The processing function never fails at the moment. I imagine we might later add some error cases but at the moment I'd just like to ignore the possible errors. However, I'm not sure how to convert from Task to TaskEither since TaskEither doesn't provide a fromTask function. I was thinking perhaps I could do .map(right) but I imagine this gives me a Task<Either<...>> instead of a TaskEither<...> and I'm not sure how to combine Task and Either into TaskEither.
Also my encoding function really consumes the entire Either since I'm working on an API that sends both success and error to the requesting client. So, I imagine I also need a way of separating the two. i.e. getting from TaskEither<...> back to Task<Either<...>> so that I can only pass the Either to the encode function, which then folds the Either and creates the response.
@cyberixae this is the general solution
import { Either } from 'fp-ts/lib/Either'
import { Task } from 'fp-ts/lib/Task'
import { fromEither, right } from 'fp-ts/lib/TaskEither'
function pipeline<Request, Err, Query, Result, Response>(
decode: (request: Request) => Either<Err, Query>,
process: (query: Query) => Task<Result>,
encode: (e: Either<Err, Result>) => Response
): (req: Request) => Task<Response> {
return request =>
fromEither(decode(request)) // `fromEither` lifts a `Either<L, A>` to a `TaskEither<L, A>`
.chain(query => right(process(query))) // `right` lifts a `Task<A>` to a `TaskEither<L, A>`
.value.map(encode) // `.value` return the inner `Task`
}
I think the biggest problem with the current documentation is that there are many things that are not directly visible in the documentation. For example if I go to Either chapter and search the page for mapLeft I don't find anything. This leads me to believe that doing mapLeft for Either is not possible.
Now that I've used fp-ts for ~6 months I have finally figured out that I have to look at export const either and discover Bifunctor2<URI> then look at Bifunctor chapter to discover mapLeft. But this is a major obstacle for people unfamiliar with fp-ts, not to mention for people unfamiliar with functional programming.
For example if I go to Either chapter and search the page for mapLeft I don't find anything
I'm working on it, I have to add support for ExportDeclarations
export {
foo,
bar
}
to docs-ts
I'm getting the following error.
Argument of type '{ readonly [I in keyof R]: Task<R[I]>; }'
is not assignable to parameter of type
'EnforceNonEmptyRecord<{ readonly [I in keyof R]: Task<R[I]>; }>'.
With arrays I would call NonEmptyArray.fromArray() but there is no such module as NonEmptyRecord. Does fp-ts expect records to always be non-empty? How do I split my Record handling to empty and non-empty?
Does fp-ts expect records to always be non-empty?
@cyberixae No, you are probably getting this error by using sequenceS or other similar functions that rely on the record being non empty (they require an instance of Apply to work, not Applicative).
This works great using it by passing the struct to sequence inline (as in sequenceS(task)({ a: ... })), but not when dealing with Record<K, A> types, since it can be empty. In this case, I'd suggest to use the plain sequence from fp-ts/lib/Record which requires an Applicative instance and thus can handle empty records.
See the diff in the signature e.g. sequenceS/Apply vs. sequence/Applicative
I feel like I need some properties of a struct and some properties of a Record. Perhaps I should be doing something completely different. I uploaded my source code to GitHub. The code has lots of generics and makes heavy use of fp-ts. It is the most complex piece of TypeScript I have written. It is a helper library that I use to iterate over queries and fill in blanks with tasks that produce the query results. It also contains symmetric set of helpers for iterating the result structure.
I got it narrowed down to this. I'm not sure if this is logically sound or if it just compiles by accident.
import * as Record_ from "fp-ts/lib/Record";
type Process = <
I extends keyof D & keyof P & string,
D extends Record<I, any>,
P extends Record<I, (x: D[I]) => any>
>(
d: D,
p: P
) => Record<I, ReturnType<P[I]>>;
const process: Process = (inputs, processors) =>
Record_.mapWithIndex((key, input) => processors[key](input))(inputs);
const result = process(
{ foo: 123, bar: "a b" },
{ foo: (x: number) => 2 * x, bar: (x: string) => x.split(" ") }
);
// { foo: 246, bar: ['a', 'b'] }
I quite often find myself writing something of the following form. I'm wondering if there is a shorter way to write such validation for test data literals that are trusted and are supposed to be valid.
const phone: Phone = pipe(
Phone.decode('+3581234567'),
Either_.getOrElse(
(_errors): Phone => {
throw new Error('assert false');
},
),
);
I also have some cases where I have checked every possible case but the type system doesn't understand the completeness of my checks and I end up writing throw new Error('assert false') at the end to get rid of the type error.
function getSpecies(bird: Duck|Goose): 'duck'|'goose' {
if (isDuck(bird)) {
return 'duck';
}
if (isGoose(bird)) {
return 'goose';
}
throw new Error('assert false');
}
@cyberixae
data literals that are trusted and are supposed to be valid
I don't even trust myself in such cases so I use Eithers everywhere even when the data is "supposed" to be valid, eventually you get used to them. The reason is you never know when it becomes invalid - you may change your Phone codec somewhen so that your string phone number is no more valid and getOrElse will throw.
Also TS actually handles your second example pretty well by narrowing bird to never after all checks so you can just return it instead of throwing:
interface Duck {
readonly _tag: 'Duck';
}
interface Goose {
readonly _tag: 'Goose';
}
type Bird = Duck | Goose;
const isDuck = (bird: Bird): bird is Duck => bird._tag === 'Duck';
const isGoose = (bird: Goose): bird is Goose => bird._tag === 'Goose';
function getSpecies(bird: Duck | Goose): 'duck' | 'goose' {
if (isDuck(bird)) {
return 'duck';
}
if (isGoose(bird)) {
return 'goose';
}
return bird; // never
}
I was mostly using the getOrElse + throw pattern since my test framework was expecting me to throw. I created a new NPM package for such unsafe operations https://github.com/maasglobal/ruins-ts
Most helpful comment
I'm working on it, I have to add support for
ExportDeclarationsto
docs-ts