Reputation: 973
While doing functional programming I often end up in situations where I know something that the type system of the language does not know. Consider the following TypeScript example that parses a UUID and shows the embedded fields to the user. The program first validates it's input with io-ts to make sure the input follows UUID specification. Later, after splitting the input, the program is unable to verify that the split UUID contains five parts which leaves me with an fp-ts Option
. It throws an assert false
from getOrElse
to get rid of the Option
. Does functional programming have some more idiomatic ways to deal with assertions? Reporting the error to the end user doesn't feel helpful since this case would be an error in underlying assumptions of the programmer rather than something that the end user could solve.
#!/usr/bin/env ts-node
import { append, intersperse, map, prepend } from 'fp-ts/lib/Array';
import { isRight } from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/function';
import { IO } from 'fp-ts/lib/IO';
import { fromPredicate, getOrElse } from 'fp-ts/lib/Option';
import { empty } from 'fp-ts/lib/string';
import * as t from 'io-ts';
type Tuple5<A, B, C, D, E> = [A, B, C, D, E];
const length = 5;
const fromArray = fromPredicate(
<A>(as: Array<A>): as is Tuple5<A, A, A, A, A> => as.length === length,
);
const Tuple5_ = {
length,
fromArray,
};
const separator = '-';
const hex = (n: number): string => `[A-Fa-f0-9]{${n}}`;
const fields: Tuple5<number, number, number, number, number> = [8, 4, 4, 4, 12];
const regexp = pipe(
fields,
map(hex),
intersperse(separator),
prepend('^'),
append('$'),
).join(empty);
export type Uuid = t.Branded<string, UuidBrand>;
export type UuidC = t.BrandC<t.StringC, UuidBrand>;
export const Uuid: UuidC = t.brand(
t.string,
(x): x is t.Branded<string, UuidBrand> => x.match(RegExp(regexp)) !== null,
'Uuid',
);
export type UuidBrand = {
readonly Uuid: unique symbol;
};
export type TimeLow = string;
export type TimeMid = string;
export type TimeHiAndVersion = string;
export type ClockSeq = string;
export type Node = string;
export type Groups = Tuple5<TimeLow, TimeMid, TimeHiAndVersion, ClockSeq, Node>;
export const groups = (uuid: Uuid): Groups =>
pipe(
uuid.split(separator),
Tuple5_.fromArray,
getOrElse((): Groups => {
// eslint-disable-next-line
throw new Error('Assert false! Uuid invalid despite validation.');
}),
);
const main: IO<void> = () => {
const [_node, _script, input] = process.argv;
const result = Uuid.decode(input);
if (isRight(result)) {
const uuid: Uuid = result.right;
const [timeLow, timeMid, timeHiAndVersion, clockSeq, node] = groups(uuid);
console.log({ timeLow, timeMid, timeHiAndVersion, clockSeq, node });
} else {
console.error('Invalid input!');
}
};
main();
Upvotes: 0
Views: 208
Reputation: 1429
Parse, don't validate.
type UuidPart1 = string & { readonly UuidPart1: unique symbol }
type UuidPart2 = string & { readonly UuidPart2: unique symbol }
type UuidPart3 = string & { readonly UuidPart3: unique symbol }
type UuidPart4 = string & { readonly UuidPart4: unique symbol }
type UuidPart5 = string & { readonly UuidPart5: unique symbol }
type SplitUuid = [UuidPart1, UuidPart2, UuidPart3, UuidPart4, UuidPart5]
declare const parseUuid: (a: Uuid) => Option<SplitUuid>
declare const recombineUuid: (a: SplitUuid) => Uuid
The former function should split the Uuid into 5 parts and then make sure each of the 5 parts conform to the format of the 5 parts of a Uuid. If they all do, then you return a Some wrapping the SplitUuid type (which is a 5-tuple). If not, return None.
Now you write any code that requires a split-up Uuid to take SplitUuid instead of Uuid.
If it doesn't require the split, you can losslessly convert back to Uuid and have the function take a Uuid param.
Now you don't need to validate. Just write code that takes the correct type and you don't have to do any runtime validation.
If you really do have something that could take a Uuid or SplitUuid, then you need a type guard:
type AnyUuid = Uuid | SplitUuid
function isSplitUuid(a: AnyUuid): a is SplitUuid {
return typeof a === 'object'
}
declare const logSplitUuid = (a: SplitUuid) => console.log('this is split!', a)
const example: (a: AnyUuid) => void = a => pipe(
O.fromPredicate(isSplitUuid),
O.getOrElse(() => parseUuid(a)),
logSplitUuid
)
That function, as an example, will take either a split or non-split Uuid, split it if it's not already split, and then log the split. Fully type safe.
Upvotes: 1