When working with functional programming, I often encounter situations where my knowledge exceeds the type system of the language. Take for example this TypeScript scenario where I parse a UUID and display its embedded fields to the user. The program initially validates the input using io-ts to ensure it adheres to the UUID specification. However, after splitting the input, the program struggles to confirm that the split UUID consists of five parts, leading me to use an fp-ts Option
. To overcome this, I resort to an assert false
from getOrElse
in order to eliminate the Option
. Are there more idiomatic ways in functional programming to handle assertions? Notifying the end user about this error doesn't seem useful as it signifies an issue in the assumptions made by the programmer rather than something the end user can resolve.
#!/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();