Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Type Your Business Logic with Flow

wuct
November 28, 2018

Type Your Business Logic with Flow

wuct

November 28, 2018
Tweet

More Decks by wuct

Other Decks in Programming

Transcript

  1. • Building from scratch is not the hardest part of

    programming but changing legacy code. • Especially when all authors are now ex-colleagues and there is no document. • Programmers are afraid to change legacy code because they do not know what will they break. • Testing can not save us, because "Testing shows the presence, not the absence of bugs.”
  2. Scenario 1: refactor a function • There is a widely

    used function in a huge code base, e.g.
 const sendMailTo = (account: string) => {}; • For some reasons, we need to make it receiving an email rather than an account:
 const sendMailTo = (email: string) => {}; • Here are some options: 1. Build a new function and keep the legacy one. 2. Change the current function and all callers, then hope tests can save us. 3. Let the type system help us!
  3. Scenario 1: how to do this? // @flow // email.js

    // Now export const sendMailTo = (account: string) => {}; // What we want export const sendMailTo = (email: string) => {}; // @flow // app.js import { sendMailTo } from './email'; sendMailTo('wuct');
  4. Type aliases can not save us // @flow // email.js

    type Email = string; export const sendMailTo = (email: Email) => {}; // @flow // app.js import { sendMailTo } from './email'; sendMailTo('wuct'); // still compiled
  5. We need opaque type aliases // @flow // email.js opaque

    type Email = string; export const sendMailTo = (email: Email) => {}; // @flow // app.js import { sendMailTo } from './email'; sendMailTo('wat'); // failed
  6. Modularization // @flow // email.js import isEmail from './isEmail'; opaque

    type Email = string; export const createEmail = (s: string): ?Email => { return isEmail(s) ? s : null; }; export const sendMailTo = (email: Email) => {}; // @flow // app.js import { sendMailTo, createEmail } from './email'; const email = createEmail('wat'); if (email) { sendMailTo(email); // compiled }
  7. More complex logic: is this email unique? // @flow //

    email.js import isEmail from './isEmail'; opaque type Email = string; export const createEmail = (s: string): ?Email => { return isEmail(s) ? s : null; }; export const isUnique = (email: Email): boolean => { // some heavy computation return email.startsWith('x'); }; export const signUp = (email: Email): boolean => { return isUnique(email); }; // @flow // app.js import { createEmail, isUnique, signUp } from './ email'; const email = createEmail('[email protected]'); // For better UX, we might want to check an email // is unique before submitting if (email && !isUnique(email)) { alert('The email is duplicated.'); } // Another part of our code base handling submitting if (email) { signUp(email); } // The isUnique is invoked twice for the same email. // Can we avoid it with modularization in mind?
  8. Using phantom types // @flow // email.js import isEmail from

    './isEmail'; opaque type Email<T> = string; class IsNotChecked {} class IsUnique {} export const createEmail = (s: string): ?Email<IsNotChecked> => { return isEmail(s) ? s : null; }; export const isUnique = (email: Email<IsNotChecked>): ? Email<IsUnique> => { // some heavy computation return email.startsWith('x') ? email : null; }; export const signUp = (email: Email<IsUnique>): boolean => { return true; }; // @flow // app.js import { createEmail, isUnique, signUp } from './ email'; const email = createEmail('[email protected]'); if (email && !isUnique(email)) { alert('The email is duplicated.'); } // Another part of our code base handling submitting if (email) { signUp(email); // failed }
  9. Using phantom types (compiled) // @flow // email.js import isEmail

    from './isEmail'; opaque type Email<T> = string; class IsNotChecked {} class IsUnique {} export const createEmail = (s: string): ?Email<IsNotChecked> => { return isEmail(s) ? s : null; }; export const isUnique = (email: Email<IsNotChecked>): ? Email<IsUnique> => { // some heavy computation return email.startsWith('x') ? email : null; }; export const signUp = (email: Email<IsUnique>): boolean => { return true; }; // @flow // app.js import { createEmail, isUnique, signUp } from './ email'; const email = createEmail('[email protected]'); const uniqueEmail = email ? isUnique(email) : null; if (!uniqueEmail) { alert('The email is duplicated or the format is wrong.'); } // Another part of our code base handling submitting if (uniqueEmail) { signUp(uniqueEmail); // compiled }
  10. More example: vehicle.js // @flow type Vehicle = 'car' |

    'bus' | 'bike'; export const wheels = (vehicle: Vehicle) => { return vehicle === 'bike' ? 2 : 4; }; export const fuel = (vehicle: Vehicle) => { if (vehicle === 'bike') throw 'error'; // fuel the vehicle }; // @flow // index.js import { wheels, fuel } from './vehicle'; const car = 'car'; wheels(car); fuel(car); const bike = 'bike'; wheels(bike); fuel(bike); // runtime error // can we do better?
  11. Example: vehicle.js with phantom types // @flow class Petrol {}

    class Pedal {} type PetrolVehicle = 'car' | 'bus'; type PedalVehicle = 'bike'; opaque type Vehicle<P> = PetrolVehicle | PedalVehicle; export const createPetrolVehicle = (t: PetrolVehicle): Vehicle<Petrol> => { return t; }; export const createPedalVehicle = (t: PedalVehicle): Vehicle<Pedal> => { return t; }; export const wheels = <P>(vehicle: Vehicle<P>) => { return vehicle === 'bike' ? 2 : 4; }; export const fuel = (vehicle: Vehicle<Petrol>) => { // only a vehicle powered by petrol can be fueled }; // @flow // index.js import { createPetrolVehicle, createPedalVehicle, wheels, fuel, } from './vehicle'; const car = createPetrolVehicle('car'); wheels(car); fuel(car); const bike = createPedalVehicle('bike'); wheels(bike); fuel(bike); // compile-time error