Fp-ts: Is there a more elegant way to write this?

Created on 23 May 2018  路  7Comments  路  Source: gcanti/fp-ts

Hey,

I have the following function which validates a contract and returns a model off of it. I would like to return an informative error string in case the conversion failed due to unexpected values in the contract.

Currently I have the following:

type Color = 'Red' | 'Green' | 'Blue'

interface PersonContract {
    name: string
    favoriteColor: string
}

interface Person {
    name: string,
    favoriteColor: Color
}

function personFromContract(contract: PersonContract): Either<string, Person> {
    return fromNullable<string>('No contract was provided')(contract)
        .chain(person => {
            if (person.favoriteColor !== 'Red' &&
                person.favoriteColor !== 'Green' &&
                person.favoriteColor !== 'Blue') {
                return left(`Unexpected color in contract: ${person.favoriteColor}`)
            }

            return right<string, Person>({
                name: person.name,
                favoriteColor: person.favoriteColor
            })
        })
}

But I feel that the following would be a more appropriate use of Either's functions.

function personFromContract(contract: PersonContract): Either<string, Person> {
    return fromNullable('No contract was provided')(contract)
        .filterOrElse(person => !(person.favoriteColor !== 'Red' &&
            person.favoriteColor !== 'Green' &&
            person.favoriteColor !== 'Blue'),
            `Unexpected color in contract: no color to refer to :(`)
        .map<Person>(person => ({
            name: person.name,
            favoriteColor: person.favoriteColor as Color
        }))
}

Is there a way to avoid the explicit if and return right/left in the first piece of code and still keep the value available for the error string? It kind of reminds me RxJs where the Observable has a do operator for side effects, where you could stash a temporary value in the closure and then use it later, but I'm not sure I like that approach if there's a more functional approach.

Thanks,
Boaz

Most helpful comment

FYI I'm writing a PR which adds a fromRefinement function to Either in order to simplify the definition of validateColor

// const validateColor: (a: string) => Either<string, Color>
const validateColor = fromRefinement(isColor, s => `Unexpected color in contract: ${s}`)

All 7 comments

I would recommend io-ts for this job. It is not only useful for validation of input/output data it can also be used to transform/validate data of similar shape.

So your code can be reduced to

const Person = t.interface({
  name: t.string,
  favoriteColor: t.keyof({ Red: true, Green: true, Blue: true })
});
type Person = t.TypeOf<typeof Person>;

const personFromContract = (contract: PersonContract): Either<string, Person> => 
  Person.decode(contract).mapLeft(() => `Unexpected color in contract: ${contract.favoriteColor}`);

You may use keyof instead of union, better error message and way faster

@sledorze thanks for the hint I adjusted my code.

@boaz-chen I would also recommend to active strict mode in your typescript project, cause your null-check at the start of the personFromContract Function should already be catched by tsc.

@boaz-chen speaking about validations, the functional way, when possible, is using "applicative validation" (<= search for this term to get some tutorials)

// curried constructor for Person

const person = (name: string) => (favoriteColor: Color): Person => ({ name, favoriteColor })

// validate each field

const validateName = (s: string): Either<string, string> => right(s)

const isColor = (s: string): s is Color => s !== 'Red' && s !== 'Green' && s !== 'Blue'

const validateColor = (s: string): Either<string, Color> =>
  isColor(s) ? right(s) : left(`Unexpected color in contract: ${s}`)

// validate all

function validatePersonContract(contract: PersonContract): Either<string, Person> {
  const fa = validateName(contract.name)
  const fb = validateColor(contract.favoriteColor)
  // lifting `person`
  return fb.ap(fa.map(person))
}

Thanks for the great info guys.

FYI I'm writing a PR which adds a fromRefinement function to Either in order to simplify the definition of validateColor

// const validateColor: (a: string) => Either<string, Color>
const validateColor = fromRefinement(isColor, s => `Unexpected color in contract: ${s}`)
Was this page helpful?
0 / 5 - 0 ratings

Related issues

mustafaekim picture mustafaekim  路  4Comments

vicrac picture vicrac  路  4Comments

josete89 picture josete89  路  3Comments

jollytoad picture jollytoad  路  4Comments

steida picture steida  路  4Comments