gcanti / fp-ts

Functional programming in TypeScript
https://gcanti.github.io/fp-ts/
MIT License
10.74k stars 503 forks source link

add ability to get value Option `fromSome()` #1096

Closed spik3r closed 4 years ago

spik3r commented 4 years ago

🚀 Feature request

Hi there. It would be great to have a concise way to get the value from an Option see suggestion below.

Suggested Solution

function fromSome<T>(optional: Option<T>): T | Option<never> {
  if (isNone(optional)) {
    return none;
  }
  return optional.value;
}

Who does this impact? Who is this for?

I've added the above snippet to my project, but adding something similar to the framework could help new users of the framework get up and running easier.

Software Version(s)
fp-ts 2.4.1
TypeScript 3.7.3
raveclassic commented 4 years ago

Could you clarify what is the usecase for such helper?

spik3r commented 4 years ago

I'm basically trying to get the value from within the optional if it exists. Otherwise return a sensible default. Similar to Java's Optional.orElse() Seems like a recurring pattern if you're using Optional to avoid nulls.

raveclassic commented 4 years ago

This is essentially getOrElse

spik3r commented 4 years ago

It seems odd that putting something into an optional is as simple as const foo = some("foo") but getting it back out seems to require a bit of ceremony every time and is a little confusing

pipe(
    foo,
    getOrElse(() => "nothing here")

Sorry if it's a noob question. Is there a simpler way to using it without needing the pipe like in the docs?

raveclassic commented 4 years ago

pipe is the idiomatic way to work with fp-ts. On practice it's better to use transformations (monadic interface: map, chain etc.) instead of manually picking data from Option's and use getOrElse in the very end of effectful chains. If you could provide some example of your problem we could help you with the solution.

spik3r commented 4 years ago

Essentially I'm calling a function that given certain inputs returns a string and otherwise doesn't. I'd prefer not to use nulls for the latter and from what I've read it looks like Option or Either are the ways to do it with fp-ts I'm open to suggestions around how you would do it as I'm new to the framework & typescript in general.

raveclassic commented 4 years ago

So you have something like this?

declare const process: (input: string) => Option<string>

Then when you deal with the result of getValue the preferred way is to use option.map or option.chain depending on the situation:

import { option } from 'fp-ts'

const result1 = pipe(
  process('foo'),
  option.map(result => {
    //       ^---- type is `string`
    return `Result: ${result}`
  })
) // type is `Option<string>`

// or sequence multiple possibly "nullable" operations
const result2 = pipe(
  process('foo'),
  option.chain(process),
  option.chain(process)
) // type is `Option<string>`
gkamperis commented 4 years ago

I think you do get used to the verbosity. And yes it is verbose.

You could write a wrapper util function that does what you want.

@raveclassic @gcanti Just a suggestion...

A thought that comes to mind is providing a similar function to toUndefined() that returns the value or a default toValueOrElse()

spik3r commented 4 years ago

Thanks @raveclassic Yea essentially that's it. @gkamperis I probably would :) Although I'd prefer some kind of wrapper to encapsulate it as you suggest since I doubt I can convince my colleagues adding a new library & then all this extra verbosity is worth it to avoid nulls, especially since most of us come from an OO background.

raveclassic commented 4 years ago

@spik3r The reasoning behind pipe starts to be clear when you work with other data structures like Either, Reader, Task, ReaderTaskEither, RemoteData etc. All of them have the same API which gracefully fits into pipe.

gcanti commented 4 years ago

A thought that comes to mind is providing a similar function to toUndefined() that returns the value or a default

@gkamperis that's getOrElse

import * as O from 'fp-ts/lib/Option'
import { pipe } from 'fp-ts/lib/pipeable'

declare const x: O.Option<string>

pipe(
  x, 
  O.toUndefined
)

pipe(
  x,
  O.getOrElse(() => '')
)
gkamperis commented 4 years ago

@gcanti I know... :)

But at same token toUndefined is

pipe(
  x,
  O.getOrElse(() => undefined )
)

Yet it still exists and is helpful because it reduces verbosity.

In my mind

toUndefined = toValueOrElse(undefined)

gcanti commented 4 years ago

@gkamperis This doesn't type-check

declare const x: O.Option<string>

pipe(
  x, // Argument of type 'Option<string>' is not assignable to parameter of type 'Option<undefined>'
  O.getOrElse(() => undefined)
)

toValueOrElse is getOrElse with "auto widening":

declare function getOrElse<B>(f: () => B): <A>(ma: O.Option<A>) => A | B

// const toUndefined: <A>(ma: O.Option<A>) => A | undefined
const toUndefined = getOrElse(() => undefined)

However I'm not a fan of auto widening functions.

Related https://github.com/gcanti/fp-ts/issues/1002

gkamperis commented 4 years ago

@gcanti I meant it as a conceptual example I did not mean it as an exact implementation.

Consider this though... it compiles:

pipe(none, getOrElse(() => undefined as unknown))

toUndefinedis defined as

export function toUndefined<A>(ma: Option<A>): A | undefined {
  return isNone(ma) ? undefined : ma.value
}

toValueOrElse

could be

export function toValueOrElse<A>(ma: Option<A>, defaultVal: A): A {
  return isNone(ma) ? defaultVal : ma.value
}
it('toValueOrElse', () => {
    assert.deepStrictEqual(O.toValueOrElse(O.none, undefined), undefined)
    assert.deepStrictEqual(O.toValueOrElse(O.none, 5), 5)
    assert.deepStrictEqual(O.toValueOrElse(O.some(1), undefined), 1)
  })
Eoksni commented 4 years ago

You don't need pipe for single function application.

getOrElse(() => "sensible default")(maybeValue)

If you don't like lazy and curried functions because of extra syntax, its not hard to write generic higher-order uncurry and unlazy functions which you can apply to getOrElse or any other lazy curried function and get back what you want.

spik3r commented 4 years ago
getOrElse(() => "sensible default")(maybeValue)

Thanks @Eoksni I think I'll go with that suggestion. :)

steida commented 4 years ago

@spik3r Read https://fr.umio.us/why-ramda/ to learn why "actionable" value is the last argument. In that example, it's really the last argument, but because of fp-ts is typed, it has to implement currying "manually" via returned functions. Functional programming "partial application" is how we can create ad-hoc "classes". By "classes" I mean something with a state. In functional programming, the state is stored in the closure of some function.