On va parler d'incertitude
dans le code

Enfin je crois... 🤨 Vous êtes bien là pour ça ?!

Je peux douter de tout, sauf d'une chose, et c'est le fait même que je doute.

René Descartes

Développeur JS (sûrement)

const results = await getResults();

const average = calculateAverage(results);
function calculateAverage(elements) {
  const sum = elements.reduce((acc, el) => acc + el, 0);
  return sum / elements.length;
}
calculateAverage([]); // 😱
function calculateAverage(elements) {
  if (elements.length === 0) {
    throw new Error('Empty array');
  }
  const sum = elements.reduce((acc, el) => acc + el, 0);
  return sum / elements.length;
}
calculateAverage(); // 😱😱
function calculateAverage(elements) {
  if (!elements) {
    throw new Error('No argument provided');
  }
  if (elements.length === 0) {
    throw new Error('Empty array');
  }
  const sum = elements.reduce((acc, el) => acc + el, 0);
  return sum / elements.length;
}
calculateAverage({}); // 🤯🤯🤯
function calculateAverage(elements) {
  if (!elements) {
    throw new Error('No argument provided');
  }
  if (!Array.isArray(elements)) {
    throw new Error('Argument is not an array');
  }
  if (elements.length === 0) {
    throw new Error('Empty array');
  }
  const sum = elements.reduce((acc, el) => acc + el, 0);
  return sum / elements.length;
}

Programmation défensive

Écrire son code sous la contrainte de la loi de Murphy

Conclusion

Il faut être complètement parano pour faire du JS 🤪

Ou faire confiance...

Fonction partielle

La fonction accepte des arguments pour lesquels elle n'a pas de valeur à retourner

Repérer une fonction partielle

Un if sans else

Un switch qui ne gère pas tous les cas

Une fonction qui throw

Fonction totale

Fonction dont l'ensemble des inputs possibles correspond à son domaine de définition

Réduisons les inputs possibles !

Il est temps d'ajouter du TypeScript !
function calculateAverage(elements: number[]) {
  if (elements === undefined) {
    throw new Error('No argument provided');
  }
  if (!Array.isArray(elements)) {
    throw new Error('Argument is not an array');
  }
  if (elements.length === 0) {
    throw new Error('Empty array');
  }
  const sum = elements.reduce((acc, el) => acc + el, 0);
  return sum / elements.length;
}
function calculateAverage(elements: number[]) {
  if (elements === undefined) {
    throw new Error('No argument provided');
  }
  if (elements.length === 0) {
    throw new Error('Empty array');
  }
  const sum = elements.reduce((acc, el) => acc + el, 0);
  return sum / elements.length;
}

strictNullChecks: true

function calculateAverage(elements: number[]) {
  if (elements.length === 0) {
    throw new Error('Empty array');
  }
  const sum = elements.reduce((acc, el) => acc + el, 0);
  return sum / elements.length;
}

Hey mais en fait un tableau non vide, c'est un tableau avec au moins un élément ! 🤔

Autrement dit : c'est
un tableau (potentiellement vide)
+ un élément

type NonEmpty<T> = { first: T, others: T[] }
type NonEmpty<T> = { first: T, others: T[] }


function calculateAverage(elements: NonEmpty<number>) {
  if (elements.length === 0) {
    throw new Error('Empty array');
  }
  const allElements = [elements.first, ...elements.others];
  const sum = allElements.reduce((acc, el) => acc + el, 0);
  return sum / allElements.length;
}
export function toArray<T>(nonEmpty: NonEmpty<T>): T[] {
  return [nonEmpty.first, ...nonEmpty.others];
}

export function length<T>(nonEmpty: NonEmpty<T>): T[] {
  return nonEmpty.others.length + 1;
}
const results = await getResults();

const average = calculateAverage(results);

Attends, que devient notre appel du coup ? 🤔

On a déplacé la responsabilité

const results = await getResults();

if (results.length === 0) {
  throw new Error('Empty array');
}

const nonEmptyResults = { 
  first: results[0], 
  others: results.slice(1) 
};

const average = calculateAverage(nonEmptyResults);
import {fromArray} from './non-empty.ts';

const results = await getResults();

const nonEmptyResults = fromArray(results);

const average = calculateAverage(nonEmptyResults);
export function fromArray<T>(elements: T[]): NonEmpty<T> {
  if (elements.length === 0) {
    throw new Error('Empty array');
  }
  return { 
    first: elements[0], 
    others: elements.slice(1) 
  };
}
Ce serait bien qu'on arrête de déplacer le problème !
export function fromArray<T>(elements: T[]): NonEmpty<T> {
  if (elements.length === 0) {
    return ???;
  }
  return { 
    first: elements[0], 
    others: elements.slice(1) 
  };
}
{ first: null, others: [] };
null
type Result<T, E> = 
    {type: "OK", value: T} 
  | {type: "FAIL", error: E}
export function ok<T, E>(value: T): Result<T, E> {
  return { type: "OK", value: value };
}

export function fail<T, E>(error: E): Result<T, E> {
  return { type: "FAIL", error: error };
}
import {ok, fail, Result} from './result.ts';

export function fromArray<T>(elements: T[])
	: Result<NonEmpty<T>, string> {
  if (elements.length === 0) {
    return fail('Empty array');
  }
  return ok({ 
    first: elements[0], 
    others: elements.slice(1) 
  });
}
import {fromArray} from './non-empty.ts';
import {map} from './result.ts';

const results = await getResults();

const nonEmptyResults = fromArray(results);

const average = map(nonEmptyResults, calculateAverage);
export function map<T, E, U>(result: Result<T, E>, mapFn: (value: T) => U)
	: Result<U, E> {
  if (result.type === "OK") {
    return { type: "OK", value = mapFn(result.value) };
  }
  return result;
}
import {withDefault} from './result.ts';

console.log(withDefault(average, 0));
export function withDefault<T, E>(result: Result<T, E>, default: T): T {
  if (result.type === "OK") {
    return value;
  }
  return default;
}

ok(value) et fail(error) sont tous les deux des valeurs

On a modifié le domaine de définition

Autre possibilité : recover

function calculateAverage(elements: number[]) {
  if (elements.length === 0) {
    return 0;
  }
  const sum = elements.reduce((acc, el) => acc + el, 0);
  return sum / elements.length;
}

"Je pense que mon argument n'est pas un tableau."

👍 TypeScript

"Je pense que cette valeur n'est jamais nulle."

👍 strictNullChecks

"Je pense que cette liste n'est pas vide."

👍 NonEmpty

"Je pense qu'il y a un cas d'erreur dans cette fonction."

👍 Result

D'autres exemples ?

Une valeur qui peut être définie ou non

Maybe / Option

function findMember(memberId: MemberId): Member | null {
  // ...
}

const member = findMember(memberId);
console.log(member?.name ?? 'Member not found')

Promise : l'erreur devient une valeur...

const results = await getResults();

const average = calculateAverage(results);

...mais await la retransforme en exception

async / await est un anti-pattern

(Cet avis n'engage que moi... 🙃)

COUPURE PROPAGANDE

Bref, d'autres exemples ?

Une liste d'éléments avec un index courant

type Quiz = {
  questions: Question[],
  currentQuestion: number,
}
type Quiz = {
  questions: NonEmpty<Question>,
  currentQuestion: number,
}
type ZipperList<T> = {
  head: T[],
  currentElement: T
  tail: T[],
}

List with Zipper

const questions = {
  head: [],
  currentElement: firstQuestion,
  tail: [secondQuestion, thirdQuestion],
}
const questions = {
  head: [firstQuestion],
  currentElement: secondQuestion,
  tail: [thirdQuestion],
}
const questions = {
  head: [firstQuestion, secondQuestion],
  currentElement: thirdQuestion,
  tail: [],
}

Email

import {ok, fail, Result} from './result.ts';

function sendEmail(email: string, content: string)
	: Result<void, string> {
  if (!isValid(email)) {
    return fail('Invalid email');
  }
  // ... actually send the email
  return ok(void);
}
import {ok, fail, Result} from './result.ts';

export class Email {
  private constructor(private email: string) {}
  
  static fromString(emailString: string): Result<Email, string> {
    if (isValid(emailString)) {
	  return fail('Invalid email');
    }
    return ok(new Email(emailString));
  }
}
function sendEmail(email: Email, content: string): void {
  // actually send email
}

Parse,
don't validate

On enrichit une donnée avec des connaissances

const email = 'hello@world.com';
if (isEmailValid(email)) {
  sendEmail(email);
}

On veut gérer nos incertitudes aux
limites de notre système

  • Quand la donnée vient d'un formulaire
  • Quand on lit un fichier
  • Quand on requête une BDD
  • Quand on reçoit de l'information d'une API

Form validation

CSV parser

ORM

GraphQL

function getUser(id: UserId): Promise<User> {
  return fetch(url)
    .then(response => {
      if (!response.ok) {
        return Promise.reject(response.statusText);
      }
      return response.json<User>();
    });
}

Les outils automatiques sont limités : ils ne peuvent pas connaître vos types métier

Gestion très manuelle

let email: Result<Email> | null = null;

emailField.addEventListener('change', target => {
  email = target.value ? Email.fromString(target.value) : null;
});
const userDecoder: Decoder<User> = Decoder.object({
  name: Decoder.string,
  age: Decoder.number,
});

const result = userDecoder.run(data);

switch (result.type) {
  case 'OK':
    // ...
  case 'FAIL':
    // ...
}
function nonEmptyDecoder(itemDecoder: Decoder<T>)
    : Decoder<NonEmpty<T>>  {
  return Decoder.array(itemDecoder)
    .then(items => NonEmpty.fromArray(items));
}
const userDecoder: Decoder<User> = Decoder.object({
  name: Decoder.string,
  age: Decoder.number,
  addresses: nonEmptyDecoder(addressDecoder),
});

Et de l'incertitude, il n'y en a pas ailleurs ?

Incertitude de contexte

users.filter(user => user.active);
users.keep(user => user.active);
users.filter(
  user => user.isActive ? "KEEP" : "DISCARD"
);
function sendItem(item: Item, withGiftWrap: bool) {
  // ...
}
function sendItem(item: Item, withGiftWrap: GiftWrapOptions) {
  // ...
}

export enum GiftWrapOptions {
  WITH,
  WITHOUT
}

Code dynamique

const userStreet = _.get('address.street.name');

Mutabilité, effets secondaires

Array.prototype.slice
Array.prototype.splice

Incertitude métier

Certaines règles ne peuvent pas être encodées dans les types.

 

Heureusement, il y a les tests !

C'est quand on commence à modéliser son incertitude qu'on réalise qu'elle est partout.

Les principales causes ?

Les connaissances implicites et les compromis de confiance.

Utiliser des types
c'est bien.

Bien les utiliser,

c'est mieux.

Typer, écrire des decoders, poser des questions au métier,
ça prend un peu de temps.

Que vous regagnez
grâce à vos certitudes.

Certains langages vous donnent davantage de certitudes.

La certitude apporte de la confiance, diminue la peur de livrer,
permet une meilleure
concentration
et une
charge cognitive réduite.

Merci !

@JoGrenat

L'incertitude dans le code

By ereold

L'incertitude dans le code

Dans nos codebases, l'incertitude est omniprésente... Fléau de nos lignes de code, elle ralentit les développements, provoque des bugs et nous rend paranoïaques ! Elle est partout mais on ne la remarque pourtant pas au premier abord... Alors, stop ! Nous allons la mettre sous le feu des projecteurs grâce au meilleur des outils de modélisation : les types. Puis, nous verrons ensemble comment lutter contre cette incertitude pour le bien de vos projets.

  • 619