inato/fp-ts-training

ifThenElse predicates take no arguments

Closed this issue · 3 comments

Thanks for sharing this! I started following along and I wonder if the following is intentional in exo0:

The implementation of ifThenElse is as follows

export const ifThenElse =
  <A>(onTrue: () => A, onFalse: () => A) =>
  (condition: boolean) =>
    condition ? onTrue() : onFalse();

Where neither of the predicates take any parameters. That means that during the piping, we need to supply the arguments (eg the current value in the Collatz sequence) as a side-effect rather than an argument. Eg

export const next: (value: number) => number = value => pipe(
  value,
  isOddP,
  ifThenElse(
    () => (value * 3) + 1,
    () => value / 2
  )
);

rather than

export const next: (value: number) => number = value => pipe(
  value,
  isOddP,
  ifThenElse(
    (x: number) => (x * 3) + 1,
    (x: number) => x / 2
  )
);

where each side of the if/else runs its predicate over the provided argument.

Quite possible I'm missing the point here but I thought I'd share.

Punie commented

Hello and thanks for the report :)

The idea is to have an fp-analog of the if (condition) { ... } else { ... } control structure here.

The reason why onTrue and onFalse are functions with no arguments and not simple values is for them to be lazy (ie. not evaluated at the call-site but only if their respective branch is chosen).

Sure the API could be a bit more powerful if ifThenElse took a predicate instead of a boolean, then it could give the input of the predicate to onTrue and onFalse. However, it would not faithfully represent the humble if (condition) { ... } else { ... } anymore.

tl;dr: yes this is a deliberate choice in this case to keep things simple and familiar to newcomers.

But I do agree that the following API would be nicer to work with:

export function ifThenElse<A, B extends A, C>(
  predicate: (a: A) => a is B,
  onTrue: (a: B) => C,
  onFalse: (a: A) => C,
): (value: A) => C;
export function ifThenElse<A, B>(
  predicate: (a: A) => boolean,
  onTrue: (a: A) => B,
  onFalse: (a: A) => B,
): (value: A) => B {
  return value => (predicate(value) ? onTrue(value) : onFalse(value));
}

Ah that makes sense, a standard if/else takes a boolean so you want to match that as close as possible. I suppose it also keeps things simpler, since there's less passing around of arguments. I think you could still evaluate onTrue or onFalse lazily if they took an argument, you'd just need to accept their input as a parameter at some point, as you did in your example, right?

Anyways though, you have other reasons for writing it as you did. Thanks for getting back to me and looking forward to trying out some more exercises 🤓

Punie commented

Closing this as answered ✅

Thanks again for the interest and feel free to open a new issue if you see other areas that are not clear or could be improved. This is very much a work in progress 😊