From 5ee006f03cc32e938aeb450ad9be3bd795d0054e Mon Sep 17 00:00:00 2001 From: vinassefranche Date: Fri, 4 Mar 2022 11:41:41 +0100 Subject: [PATCH] Add the first exercices on traverse --- src/exo5/exo5.test.ts | 41 ++++++++++++++ src/exo5/exo5.ts | 123 ++++++++++++++++++++++++++++++++++++++++++ src/exo5/index.ts | 1 + 3 files changed, 165 insertions(+) create mode 100644 src/exo5/exo5.test.ts create mode 100644 src/exo5/exo5.ts create mode 100644 src/exo5/index.ts diff --git a/src/exo5/exo5.test.ts b/src/exo5/exo5.test.ts new file mode 100644 index 0000000..c074008 --- /dev/null +++ b/src/exo5/exo5.test.ts @@ -0,0 +1,41 @@ +import { option } from 'fp-ts'; +import { + getValidCountryCodeOfCountryNames, + giveCurrencyOfCountryToUser, +} from './exo5'; + +describe('exo5', () => { + describe('giveCurrencyOfCountryToUser', () => { + it('should return Some if provided string is "France"', async () => { + const result = await giveCurrencyOfCountryToUser('France')(); + + expect(result).toStrictEqual(option.some('EUR')); + }); + + it('should return Some if provided string is "USA"', async () => { + const result = await giveCurrencyOfCountryToUser('USA')(); + + expect(result).toStrictEqual(option.some('DOLLAR')); + }); + + it('should return None if provided string is "Germany"', async () => { + const result = await giveCurrencyOfCountryToUser('Germany')(); + + expect(result).toStrictEqual(option.none); + }); + }); + + describe('getValidCountryCodeOfCountryNames', () => { + it('should return a Some of the country codes if all the country names are valid', () => { + const result = getValidCountryCodeOfCountryNames(['France', 'Spain']); + + expect(result).toStrictEqual(option.some(['FR', 'SP'])); + }); + + it('should return a None if any of the country names is not valid', () => { + const result = getValidCountryCodeOfCountryNames(['France', 'Germany']); + + expect(result).toStrictEqual(option.none); + }); + }); +}); diff --git a/src/exo5/exo5.ts b/src/exo5/exo5.ts new file mode 100644 index 0000000..77257a1 --- /dev/null +++ b/src/exo5/exo5.ts @@ -0,0 +1,123 @@ +// `fp-ts` training Exercice 5 +// Managing nested effectful data with `traverse` + +import { option, readonlyRecord, task } from 'fp-ts'; +import { 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 { unimplemented, unimplementedAsync } from '../utils'; + +// TBD + +/////////////////////////////////////////////////////////////////////////////// +// 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 so the signature is +// `getCountryCurrency: (countryCode: CountryCode) => Task` +type Currency = 'EUR' | 'DOLLAR'; +export const getCountryCurrency = + (countryCode: CountryCode): Task => + async () => { + if (countryCode === 'US') { + return 'DOLLAR'; + } + return 'EUR'; + }; + +// Let's simulate a request to the user to provide a country name +// Let's consider that it cannot fail and let's add the possibility to set +// user's response as a parameter for easier testing +// `getCountryNameFromUser: (countryName: string) => Task` +export const getCountryNameFromUser = (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 +// `getCountryCode: (countryName: string) => Option` +export const getCountryCode = (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, +) => + 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. + +// Use traverse to implement giveCurrencyOfCountryToUser below which returns +// a Task>. +// +// HINT: Take a look at `option.traverse` to transform an `Option` to +// a `Task