// `fp-ts` training Exercise 5 // Managing nested effectful data with `traverse` import { option, readonlyRecord, task, taskOption } from "fp-ts"; import { flow, pipe } from "fp-ts/lib/function"; import { Option } from "fp-ts/lib/Option"; import { ReadonlyRecord } from "fp-ts/lib/ReadonlyRecord"; import { Task } from "fp-ts/lib/Task"; import { sleep } from "../utils"; // When using many different Functors in a complex application, we can easily // get to a point when we have many nested types that we would like to 'merge', // like `Task>>` or `Either>>` // It would be nice to have a way to 'move up' the similar types in order to // chain them, like merging the `Task` to have a `Task>` or the // `Either` to have a `Either>` // // That's precisely the concept of `traverse`. It will allow us to transform // a `Option>` to a `Task>` so we can chain it with another // `Task` for example, or to transform a `ReadonlyArray>` to a // `Either>` /////////////////////////////////////////////////////////////////////////////// // SETUP // /////////////////////////////////////////////////////////////////////////////// // Let's consider a small range of countries (here, France, Spain and the USA) // with a mapping from their name to their code: type CountryCode = "FR" | "SP" | "US"; export const countryNameToCountryCode: ReadonlyRecord = { France: "FR", Spain: "SP", USA: "US", }; // Let's simulate the call to an api which would return the currency when // providing a country code. For the sake of simplicity, let's consider that it // cannot fail. type Currency = "EUR" | "DOLLAR"; export const getCountryCurrency: (countryCode: CountryCode) => Task = (countryCode: CountryCode): Task => async () => { if (countryCode === "US") { return "DOLLAR"; } return "EUR"; }; // Let's simulate a way for the user to provide a country name. // Let's consider that it cannot fail and let's add the possibility to set // the user's response as a parameter for easier testing. export const getCountryNameFromUser: (countryName: string) => Task = ( countryName: string, ) => task.of(countryName); // Here's a function to retrieve the countryCode from a country name if it is // matching a country we support. This method returns an `Option` as we cannot // return anything if the given string is not matching a country name we know export const getCountryCode: (countryName: string) => Option = ( countryName: string, ) => readonlyRecord.lookup(countryName)(countryNameToCountryCode); /////////////////////////////////////////////////////////////////////////////// // TRAVERSING OPTIONS // /////////////////////////////////////////////////////////////////////////////// // With all these functions, we can simulate a program that would ask for a // country name and return its currency if it knows the country. // A naive implementation would be mapping on each `Task` and `Option` to call // the correct method: export const naiveGiveCurrencyOfCountryToUser: ( countryNameFromUserMock: string, ) => Task>> = (countryNameFromUserMock) => pipe( getCountryNameFromUser(countryNameFromUserMock), task.map(getCountryCode), task.map(option.map(getCountryCurrency)), ); // The result type of this method is: `Task>>` // Not ideal, right? We would need to await the first `Task`, then check if it's // `Some` to get the `Task` inside and finally await the `Task` to retrieve the // currency. // Let's do better than that! // First we need a way to transform our `Option>` to // `Task>` // That's precisely what traverse is about. // Use `option.traverse` to implement `getCountryCurrencyOfOptionalCountryCode` // below. This function takes an `Option`, should apply // `getCountryCurrency` to the `CountryCode` and make it so that the result // is `Task>` // // HINT: `option.traverse` asks for an Applicative as the first parameter. You // can find it for `Task` in `task.ApplicativePar` export const getCountryCurrencyOfOptionalCountryCode: ( optionalCountryCode: Option, ) => Task> = option.traverse(task.ApplicativePar)( getCountryCurrency, ); // Let's now use this function in our naive implementation's pipe to see how it // improves it. // Implement `giveCurrencyOfCountryToUser` below so that it returns a // `Task>` // // HINT: You should be able to copy the pipe from naiveGiveCurrencyOfCountryToUser // and make only few updates of it. The `task.chain` helper may be useful. export const giveCurrencyOfCountryToUser: ( countryNameFromUserMock: string, ) => Task> = flow( getCountryNameFromUser, task.map(getCountryCode), task.chain(getCountryCurrencyOfOptionalCountryCode), ); // BONUS: We don't necessarily need `traverse` to do this. Try implementing // `giveCurrencyOfCountryToUser` by lifting some of the functions' results to // `TaskOption` export const giveCurrencyOfCountryToUser2: ( countryNameFromUserMock: string, ) => Task> = flow( getCountryNameFromUser, task.map(getCountryCode), taskOption.flatMapTask(getCountryCurrency), ); /////////////////////////////////////////////////////////////////////////////// // TRAVERSING ARRAYS // /////////////////////////////////////////////////////////////////////////////// // Let's say we want to ask the user to provide multiple countries. We'll have an array // of country names as `string` and we want to retrieve the country code of each. // Looks pretty easy: export const getCountryCodeOfCountryNames = ( countryNames: ReadonlyArray, ) => countryNames.map(getCountryCode); // As expected, we end up with a `ReadonlyArray>`. We know for // each item of the array if we have been able to find the corresponding country // code or not. // While this can be useful, you need to handle the option anytime you want to // perform any operation on each country code (let's say you want get the currency // of each) // It would be easier to 'merge' all the options into one and have a `Some` only if // all the country codes are `Some` and a `None` if at least one is `None`. // Doing this allows you to stop the process if you have a `None` to tell the user // that some countries are not valid or move on with a `ReadonlyArray>` // if all are valid. // Type-wise, it means going from `ReadonlyArray>` to // `Option>` // This is what traversing array is about. // Let's write a method that gets the country code for each element of an array // of country names and returns an option of an array of country codes. // // HINT: while `readonlyArray.traverse` exists, you have a shortcut in the `option` // module: `option.traverseArray` export const getValidCountryCodeOfCountryNames: ( countryNames: ReadonlyArray, ) => Option> = option.traverseArray(getCountryCode); /////////////////////////////////////////////////////////////////////////////// // TRAVERSING ARRAYS ASYNCHRONOUSLY // /////////////////////////////////////////////////////////////////////////////// // We've seen how to traverse an `array` of `option`s but this is not something // specific to `option`. We can traverse an `array` of any applicative functor, // like `either` or `task` for example. // When dealing with functors that perform asynchronous side effects, like //`task`, comes the question of parallelization. Do we want to run the // computation on each item of the array in parallel or one after the other? // Both are equally feasible with fp-ts, let's discover it! // Let's simulate a method that reads a number in a database, does some async // computation with it, replaces this number in the database by the result of // the computation and returns it const createSimulatedAsyncMethod = (): (toAdd: number) => Task => { let number = 0; return (toAdd: number) => async () => { const currentValue = number; await sleep(100); number = currentValue + toAdd; return number; }; }; // Write a method to traverse an array by running the method // `simulatedAsyncMethodForParallel: (toAdd: number) => Task` // defined below on each item in parallel. // // HINT: as was the case for `option`, you have a few helpers in the `task` // module to traverse arrays export const simulatedAsyncMethodForParallel = createSimulatedAsyncMethod(); export const performAsyncComputationInParallel: ( numbers: ReadonlyArray, ) => Task> = task.traverseArray( simulatedAsyncMethodForParallel, ); // Write a method to traverse an array by running the method // `simulatedAsyncMethodForSequence: (toAdd: number) => Task` // defined below on each item in sequence. // // HINT: as was the case for `option`, you have a few helpers in the `task` // module to traverse arrays export const simulatedAsyncMethodForSequence = createSimulatedAsyncMethod(); export const performAsyncComputationInSequence: ( numbers: ReadonlyArray, ) => Task> = task.traverseSeqArray( simulatedAsyncMethodForSequence, ); /////////////////////////////////////////////////////////////////////////////// // SEQUENCE // /////////////////////////////////////////////////////////////////////////////// // `traverse` is nice when you need to get the value inside a container (let's // say `Option`), apply a method to it that return another container type (let's // say `Task`) and 'invert' the container (to get a `Task