Back to flin
flin

Tuples, enums et structs

Comment nous avons conçu les trois structures de données fondamentales de FLIN -- les tuples pour le regroupement anonyme, les enums pour les alternatives nommées et les structs d'entités pour les enregistrements persistants.

Thales & Claude | March 30, 2026 11 min flin
EN/ FR/ ES
flintuplesenumsstructsdata-types

Chaque langage de programmation a besoin d'un moyen de regrouper des données. La question est combien de façons, et quels compromis chacune fait.

FLIN en a trois : les tuples, les enums et les entity structs. Chacun sert un objectif distinct, et les frontières entre eux sont délibérées. Les tuples regroupent des données anonymes et ordonnées. Les enums définissent des alternatives nommées. Les entity structs définissent des enregistrements persistants avec des champs nommés. Comprendre pourquoi FLIN a exactement ces trois -- et pas deux, ou quatre -- nécessite de comprendre ce que chacun fait que les autres ne peuvent pas.

Tuples : anonymes, ordonnés, légers

Les tuples ont été ajoutés à la Session 142. La motivation était simple : parfois on a besoin de retourner deux valeurs d'une fonction, et créer une entité pour cela est excessif.

flin// Littéral tuple
point = (10, 20)
rgb = (255, 128, 0)
pair = ("Juste", 25)

// Tuple typé
coordinates: (int, int) = (10, 20)
mixed: (text, int, bool) = ("hello", 42, true)

// Accès par index
x = point.0    // 10
y = point.1    // 20

Un tuple est une collection de taille fixe de valeurs qui peuvent avoir des types différents. Le type (int, text) signifie « une paire où le premier élément est un int et le second est un text ». Contrairement à une liste, la longueur et les types d'éléments d'un tuple sont connus à la compilation.

Représentation du type tuple

Dans l'AST, les tuples sont une nouvelle variante de type :

rustpub enum Type {
    // ... existing variants ...
    Tuple(Vec<Type>),  // (int, text, bool)
}

Et dans le vérificateur de types :

rustpub enum FlinType {
    // ... existing variants ...
    Tuple(Vec<FlinType>),
}

La vérification de type sur les tuples vérifie la compatibilité élément par élément :

rustfn types_compatible(&self, expected: &FlinType, actual: &FlinType) -> bool {
    match (expected, actual) {
        (FlinType::Tuple(expected_elems), FlinType::Tuple(actual_elems)) => {
            expected_elems.len() == actual_elems.len()
                && expected_elems.iter().zip(actual_elems.iter())
                    .all(|(e, a)| self.types_compatible(e, a))
        }
        // ...
    }
}

La longueur et chaque type d'élément doivent correspondre. (int, text) n'est pas compatible avec (text, int) -- l'ordre compte.

Déstructuration de tuples

Les tuples interagissent naturellement avec la déstructuration :

flin(x, y) = get_coordinates()
(name, age, active) = ("Juste", 25, true)

// Dans les retours de fonction
fn divide(a: int, b: int) -> (int, int) {
    quotient = a / b
    remainder = a % b
    return (quotient, remainder)
}

(q, r) = divide(17, 5)
// q = 3, r = 2

Le motif de déstructuration (x, y) reflète le littéral tuple (10, 20). Le côté gauche et le côté droit ont la même forme. Le compilateur les met en correspondance par position et lie chaque variable.

Quand utiliser les tuples vs les entités

Les tuples sont pour les données anonymes et éphémères. Les utiliser quand : - On retourne plusieurs valeurs d'une fonction - On regroupe des valeurs temporairement dans un calcul - Les données n'ont pas de noms de champs significatifs

Les entités sont pour les données nommées et persistantes. Les utiliser quand : - Les données ont une signification dans le domaine (User, Product, Order) - Les champs ont des noms significatifs (name, email, price) - Les données seront stockées dans une base de données

La règle empirique : si on nommerait les champs first, second, third, utiliser un tuple. Si on les nommerait name, age, active, utiliser une entité.

Enums : alternatives nommées

Les enums de FLIN ont évolué à travers plusieurs sessions, des enums de valeur simples aux unions étiquetées complètes avec génériques. Cette section couvre l'enum de base -- la fondation que les unions étiquetées étendent.

Enums simples

Un enum simple définit un ensemble de valeurs nommées :

flinenum Direction {
    North,
    South,
    East,
    West
}

enum Status {
    Pending,
    Active,
    Suspended,
    Deleted
}

Chaque variante est une valeur distincte. Direction.North n'est pas égal à Direction.South. On ne peut pas assigner une Direction à une variable Status. Le compilateur les traite comme des types complètement séparés.

Représentation des enums

Dans l'AST :

rustpub struct EnumVariant {
    pub name: String,
    pub data_type: Option<Type>,
    pub span: Span,
}

Stmt::EnumDecl {
    name: String,
    type_params: Vec<TypeParam>,
    variants: Vec<EnumVariant>,
    visibility: Visibility,
    span: Span,
}

Pour les enums simples, chaque variante a data_type: None. Le vérificateur de types enregistre l'enum et ses variantes :

rustfn register_enum(&mut self, name: &str, variants: &[EnumVariant]) {
    let variant_names: Vec<String> = variants.iter()
        .map(|v| v.name.clone())
        .collect();

    // Register the enum type
    self.type_env.insert(
        name.to_string(),
        FlinType::Enum {
            name: name.to_string(),
            type_params: vec![],
            variants: variant_names.iter()
                .map(|n| (n.clone(), None))
                .collect(),
        },
    );

    // Register each variant as a value
    for variant in &variant_names {
        self.value_env.insert(
            format!("{}.{}", name, variant),
            FlinType::Enum {
                name: name.to_string(),
                type_params: vec![],
                variants: vec![(variant.clone(), None)],
            },
        );
    }
}

Chaque variante est enregistrée à la fois comme partie du type enum et comme valeur individuelle. Cela permet d'utiliser Direction.North comme valeur et Direction comme type.

Méthodes d'enum

Les enums peuvent avoir des méthodes associées :

flinenum Direction {
    North,
    South,
    East,
    West
}

fn Direction.opposite() -> Direction {
    match self {
        North -> South
        South -> North
        East -> West
        West -> East
    }
}

heading = Direction.North
reverse = heading.opposite()  // Direction.South

Le match à l'intérieur de la méthode est exhaustif -- chaque variante est gérée. Si une nouvelle variante est ajoutée à l'enum, le compilateur signalera chaque méthode qui ne la gère pas.

Filtrage sur les enums

Le pattern matching sur les enums simples vérifie la variante :

flinfn describe(dir: Direction) -> text {
    match dir {
        North -> "heading north"
        South -> "heading south"
        East -> "heading east"
        West -> "heading west"
    }
}

Le compilateur vérifie l'exhaustivité : chaque variante doit être couverte. C'est la garantie de sécurité fondamentale des enums -- on ne peut pas oublier un cas.

Entity structs : nommés, persistants, relationnels

Les entités sont la structure de données principale de FLIN. Elles servent à la fois de types et de tables de base de données :

flinentity User {
    name: text
    email: text
    age: int = 0
    bio: text? = none
    role: text = "user"
    created: time = now
}

Cette déclaration crée : - Un type appelé User - Une table de base de données appelée users - Des opérations CRUD : User.all, User.find(id), User.where(...), save, delete - Des accesseurs de champs : user.name, user.email, etc.

Construction d'entité

Les entités sont construites avec des champs nommés :

flinuser = User {
    name: "Juste",
    email: "[email protected]",
    age: 25
}

Le vérificateur de types valide la construction :

rustfn check_entity_construction(
    &mut self,
    entity_name: &str,
    fields: &[(String, Expr)],
) -> FlinType {
    let entity_def = self.get_entity_def(entity_name);

    // Check all required fields are provided
    for field in &entity_def.fields {
        if field.default.is_none() && field.optional == false {
            let provided = fields.iter().any(|(name, _)| name == &field.name);
            if !provided {
                self.report_error(&format!(
                    "missing required field '{}' in {} construction",
                    field.name, entity_name
                ));
            }
        }
    }

    // Check all provided fields exist and have correct types
    for (name, value) in fields {
        match entity_def.fields.iter().find(|f| &f.name == name) {
            Some(field_def) => {
                let value_type = self.infer_type(value);
                if !self.types_compatible(&field_def.field_type, &value_type) {
                    self.report_error(&format!(
                        "field '{}' expects {}, got {}",
                        name, field_def.field_type, value_type
                    ));
                }
            }
            None => {
                self.report_error(&format!(
                    "unknown field '{}' in {}",
                    name, entity_name
                ));
            }
        }
    }

    FlinType::Entity(entity_name.to_string())
}

Trois validations : les champs requis doivent être présents, les champs fournis doivent exister, et les valeurs de champs doivent avoir le type correct.

Relations d'entité

Les entités peuvent référencer d'autres entités :

flinentity User {
    name: text
    posts: [Post]      // un-à-plusieurs
}

entity Post {
    title: text
    content: text
    author: User       // plusieurs-à-un
}

Le vérificateur de types résout ces références. user.posts a le type [Post]. post.author a le type User. post.author.name a le type text. La chaîne d'accès aux champs est vérifiée au niveau des types à chaque étape.

Entité vs tuple : le test du nommage

La différence pratique entre les entités et les tuples est le nommage. Considérons :

flin// Tuple -- anonyme, positionnel
point = (10, 20)
x = point.0       // qu'est-ce que 0 ? Largeur ? Latitude ? Colonne ?

// Entité -- nommée, auto-documentée
entity Point { x: int, y: int }
point = Point { x: 10, y: 20 }
x = point.x       // sans ambiguïté

La version entité est plus longue. Elle est aussi impossible à mal comprendre. Quand quelqu'un lit point.x, il sait exactement ce qu'il accède. Quand il lit point.0, il doit vérifier la définition du tuple pour savoir ce que la position 0 signifie.

La comparaison à trois voies

FonctionnalitéTupleEnumEntité
Champs nommésNonNon (les variantes sont nommées)Oui
Types multiplesOuiOui (chaque variante)Oui
PersistantNonNonOui
Pattern matchingPar positionPar variantePar nom de champ
MutabilitéImmuableImmuableMutable
RelationsNonNonOui (un-à-plusieurs, etc.)
Table de BDDNonNonOui

Les tuples sont des groupes éphémères. Les enums sont des alternatives type-safe. Les entités sont des enregistrements persistants. Chacun a un rôle clair, et utiliser le bon pour le bon usage rend les programmes FLIN plus clairs.

Combiner les trois

Les vrais programmes FLIN utilisent les trois ensemble :

flinenum Shape {
    Circle(number),
    Rectangle(number, number),
    Point
}

entity Drawing {
    name: text
    shapes: [Shape]
    origin: (int, int)
    created: time = now
}

fn bounding_box(shape: Shape) -> (number, number) {
    match shape {
        Circle(r) -> (r * 2, r * 2)
        Rectangle(w, h) -> (w, h)
        Point -> (0, 0)
    }
}

drawing = Drawing {
    name: "My Drawing",
    shapes: [Circle(5.0), Rectangle(10.0, 20.0), Point],
    origin: (0, 0)
}

for shape in drawing.shapes {
    (width, height) = bounding_box(shape)
    print("Bounding box: " + text(width) + " x " + text(height))
}

L'enum Shape définit les alternatives (cercle, rectangle, point). L'entité Drawing définit un enregistrement persistant. La fonction bounding_box retourne un tuple. La boucle for déstructure le tuple. Les trois structures de données fonctionnent ensemble.

Implémentation à travers les sessions

Les trois structures de données ont été implémentées à travers plusieurs sessions :

  • Entités : Partie du cœur de FLIN depuis les premières sessions. Le mot-clé entity et ses opérations de base de données associées ont été la première fonctionnalité majeure.
  • Enums : La Session 048 a ajouté les enums simples. La Session 145 les a étendus aux unions étiquetées avec génériques.
  • Tuples : La Session 142 a ajouté les types tuple, les littéraux et la déstructuration.

Chaque fonctionnalité a été conçue en tenant compte des autres. La déstructuration de tuples utilise le même enum Pattern que la déstructuration d'entité. Le pattern matching sur les enums utilise la même infrastructure d'expression match que le filtrage sur les tuples et les entités. La génération de code pour les trois partage des motifs communs pour l'accès aux valeurs et les liaisons.

Principes de conception

Trois principes ont guidé la conception des structures de données de FLIN :

Orthogonalité. Chaque structure de données fait une chose. Les tuples regroupent. Les enums alternent. Les entités persistent. Il n'y a pas de structure de données qui essaie de faire les trois.

Composabilité. Les structures de données se composent les unes avec les autres. Une entité peut avoir un champ de type enum. Un tuple peut contenir des entités. Une variante d'enum peut porter un tuple. Toute combinaison est valide.

Complexité progressive. Un débutant commence avec les entités -- la structure de données la plus courante et la plus utile. Les tuples et les enums sont là quand on en a besoin mais ne sont pas requis pour les programmes simples. La courbe d'apprentissage est graduelle.

Ces principes reflètent la philosophie plus large de FLIN : rendre le cas courant simple et le cas avancé possible. La plupart des programmes ont besoin d'entités. Certains ont besoin d'enums. Moins ont besoin de tuples. Mais quand on a besoin de n'importe lequel d'entre eux, ils sont là, bien conçus et interopérables.

Le prochain article couvre les gardes de type et le rétrécissement de type à l'exécution -- le mécanisme qui rend le travail avec ces divers types de données sûr à chaque point du programme.


Ceci est la partie 39 de la série « Comment nous avons construit FLIN », documentant comment un CEO à Abidjan et un CTO IA ont conçu et implémenté un langage de programmation à partir de zéro.

Navigation de la série : - [37] La déstructuration partout - [38] L'opérateur pipeline : composition fonctionnelle dans FLIN - [39] Tuples, enums et structs (vous êtes ici) - [40] Gardes de type et rétrécissement de type à l'exécution - [41] Le type Never et la vérification d'exhaustivité

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles