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 user = await getUser(userId);

const average = calculateAverage(user.grades);
function calculateAverage(grades) {
  const sum = grades.reduce((acc, el) => acc + el, 0);
  return sum / grades.length;
}
calculateAverage([]); // 😱
function calculateAverage(grades) {
  if (grades.length === 0) {
    throw new Error('Empty array');
  }
  const sum = grades.reduce((acc, el) => acc + el, 0);
  return sum / grades.length;
}
calculateAverage(null); // 😱😱

Manquer de confiance en soi, c'est croire que l'on est 

null
function calculateAverage(grades) {
  if (!grades) {
    throw new Error('No argument provided');
  }
  if (grades.length === 0) {
    throw new Error('Empty array');
  }
  const sum = grades.reduce((acc, el) => acc + el, 0);
  return sum / grades.length;
}
calculateAverage({}); // 🤯🤯🤯
function calculateAverage(grades) {
  if (!grades) {
    throw new Error('No argument provided');
  }
  if (!Array.isArray(grades)) {
    throw new Error('Argument is not an array');
  }
  if (grades.length === 0) {
    throw new Error('Empty array');
  }
  const sum = grades.reduce((acc, el) => acc + el, 0);
  return sum / grades.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...

Photo by Moose Photos

Fonction partielle

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

Inputs possibles

Ensemble de définition

Exceptions

ou valeurs nulles

 😱

Repérer une fonction partielle

Un if sans else

Un switch qui ne gère pas tous les cas

Une fonction qui throw

Inputs possibles

Ensemble de définition

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(grades: number[]) {
  if (!grades) {
    throw new Error('No argument provided');
  }
  if (!Array.isArray(grades)) {
    throw new Error('Argument is not an array');
  }
  if (grades.length === 0) {
    throw new Error('Empty array');
  }
  const sum = grades.reduce((acc, el) => acc + el, 0);
  return sum / grades.length;
}
function calculateAverage(grades: number[]) {
  if (grades === undefined) {
    throw new Error('No argument provided');
  }
  if (grades.length === 0) {
    throw new Error('Empty array');
  }
  const sum = grades.reduce((acc, el) => acc + el, 0);
  return sum / grades.length;
}

strictNullChecks: true

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

Augmentons le domaine de définition

Recover

function calculateAverage(grades: number[]) {
  if (grades.length === 0) {
    return 0;
  }
  const sum = grades.reduce((acc, el) => acc + el, 0);
  return sum / grades.length;
}
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 };
}

L'erreur comme valeur

function calculateAverage(grades: number[]): Result<string, number> {
  if (grades.length === 0) {
    return fail('No grade in the array');
  }
  const sum = grades.reduce((acc, el) => acc + el, 0);
  return ok(sum / grades.length);
}

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

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

Promise : l'erreur devient une valeur...

const user = await getUser(userId);

const average = calculateAverage(user.grades);

...mais await la retransforme en exception

async / await est un anti-pattern

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

COUPURE PROPAGANDE

Base de données

Formulaires

Réseau

Never Trust User Input

Appel d'API

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

Base de données

gamesRouter.get("/", async (_req: Request, res: Response) => {
  try {
    const games = (await collections.games.find({}).toArray())
       as Game[];
    res.status(200).send(games);
  } catch (error) {
    res.status(500).send(error.message);
  }
});

Données utilisateur

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

emailField.addEventListener('change', target => {
  if (target.value && isEmailValid(target.value)) {
    email = target.value;
  } else {
    email = null;
  }
});
app.post('/send-email', function(req, res) {
  if (!isEmailValid(req.body.email)) {
    res.status(404);
    return;
  }
  sendEmail(req.body.email);
  res.status(201);
});

Validation plus avancée via annotations / packages spécialisés

Contrôle aux frontières

Base de données

Formulaires

Réseau

Never Trust Anything

function getUser(id: UserId): Promise<User> {
  return fetch(url)
    .then(response => {
      if (!response.ok) {
        return Promise.reject(response.statusText);
      }
      return response.json<unknown>();
    })
    .then(result => {
      if (typeof result !== "object") {
        return Promise.reject('Invalid user');
      }
      if (!result.email || !isEmailValid(result.email)) {
        return Promise.reject('Invalid email');
      }
      if (!result.grades || !Array.isArray(result.grades)) {
        return Promise.reject('No grades found');
      }
      // ...
      return Promise.resolve(result as User);
    });
}

😭

const userSchema = {
  title: "User",
  description: "A user",
  type: "object",
  properties: {
    id: {
      type: "integer"
    },
    email: {
      type: "string",
      pattern: "^\S+@\S+\.\S+$"
    },
    grades: {
      type: "array",
      items: {
        type: "integer"
      }
    }
  },
  required: [ "id", "email", "grades" ]
};

const ajv = new Ajv()
const userValidate = ajv.compile(userSchema)
function getUser(id: UserId): Promise<User> {
  return fetch(url)
    .then(response => {
      if (!response.ok) {
        return Promise.reject(response.statusText);
      }
      return response.json<unknown>();
    })
    .then(result => {
      if (!userValidate(result)) {
        return Promise.reject('Invalid user');
      }
      return Promise.resolve(result as User);
    });
}

GraphQL

const query = gql`
  query GetUser {
    id
    email
    grades
 }
`;

GraphQL

const UserView: React.FC<UserViewProps> = () => {
  const {data, loading, error} = 
    useQuery<GetUser, GetUserVariables>(query);

  if (loading) return <Loading />;
  if (error) return <p>Error</p>;
  if (!data) return <p>Not found</p>;

  return (
    <p>
      User ID: {data.user.id}
    </p>
  );
}

👍

type User = {
  id: number;
  email: string;
  grades: number[];
}
sendEmail(user.email, emailContent);

Email

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

La vérification doit être faite partout.

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
}

On transforme des données moins structurées

en données plus structurées

Parse,
don't validate

On enrichit une donnée avec des connaissances

const email = Email.fromString('hello@world.com');

if (email.type === 'OK') {
  const validatedEmail = email.value;
} else {
  // ...
}
  • JSON
  • Base de données
  • Champ de formulaire (string)
  • Fichier
function calculateAverage(grades: number[]) {
  if (grades.length === 0) {
    throw new Error('Empty array');
  }
  const sum = grades.reduce((acc, el) => acc + el, 0);
  return sum / grades.length;
}
class NonEmpty<T>() {
  private constructor(private elements: T[]) {}
  
  static fromArray<T>(e: T[]): Result<String, NonEmpty<T>> {
    if (elements.length > 0)  {
      return ok(new NonEmptyArray(e));
    }
    return fail('Empty list');
  }
}

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[] }
class NonEmpty<T>() {
  private constructor(public head: T, public tail: T[]) {}
  
  static fromArray<T>(e: T[]): Result<String, NonEmpty<T>> {
    if (elements.length > 0)  {
      return ok(new NonEmptyArray(e[0], e.slice(1)));
    }
    return fail('Empty list');
  }
}
type NonEmpty<T> = { first: T, others: T[] }


function calculateAverage(elements: NonEmpty<number>) {
  if (elements.length === 0) {
    throw new Error('Empty array');
  }
  const allElements = [elements.head, ...elements.tail];
  const sum = allElements.reduce((acc, el) => acc + el, 0);
  return sum / allElements.length;
}
const user = await getUser(userId);

const average = calculateAverage(user.grades);

Attends, que devient notre appel du coup ? 🤔

On a déplacé la responsabilité

const user = await getUser();

const grades = NonEmpty.fromArray(user.grades);

const average = grades.type === 'OK' 
	? calculateAverage(nonEmptyResults)
	: 0;

Idéalement, on veut gérer nos incertitudes
aux limites de notre système

const userDecoder: Decoder<User> = Decoder.object({
  id: Decoder.number,
});

const result = userDecoder.run(data);

switch (result.type) {
  case 'OK':
    // ...
  case 'FAIL':
    // ...
}
function emailDecoder(): Decoder<Email>  {
  return Decoder.string.then(Email.fromString);
}
const userDecoder: Decoder<User> = Decoder.object({
  id: Decoder.number,
  email: emailDecoder
});
function nonEmptyDecoder(itemDecoder: Decoder<T>)
    : Decoder<NonEmpty<T>>  {
  return Decoder.array(itemDecoder)
    .then(NonEmpty.fromArray);
}
const userDecoder: Decoder<User> = Decoder.object({
  id: Decoder.number,
  email: emailDecoder,
  grades: nonEmptyDecoder(Decoder.number),
});

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

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: [],
}

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

Made with Slides.com