Formation F#
  • Intro
  • Bases
    • Le F♯, c'est quoi ?
    • Syntaxe
    • Premiers concepts
    • 🍔 Quiz
  • Fonctions
    • Signature
    • Fonctions
    • Fonctions standard
    • Opérateurs
    • Fonctions : compléments
    • 🍔 Quiz
    • 📜 Récap’
  • Types composites
    • Généralités
    • Tuples
    • Records
    • Unions
    • Enums
    • Records anonymes
    • Types valeur
    • 🍔 Quiz
  • Types : Compléments
    • Type unit
    • Génériques
    • Types flexibles
    • Unités de mesure
    • Conversion
    • Exceptions F#
  • Pattern matching
    • Patterns
    • Match expression
    • 🚀 Active Patterns
    • 📜 Récap’
  • Collections
    • Vue d'ensemble
    • Types
    • Fonctions génériques
    • Fonctions spécifiques
    • 🍔 Quiz
    • 📜 Récap’
  • Programmation asynchrone
    • Workflow asynchrone
    • Interop avec la TPL .NET
    • 📜 Récap’
  • Types monadiques
    • Type Option
    • Type Result
    • Smart constructor
    • 🚀 Computation expression (CE)
    • 🚀 CE - Fondements théoriques
    • 📜 Récap’
  • Module & namespace
    • Vue d'ensemble
    • Namespace
    • Module
    • 🍔 Quiz
    • 📜 Récap’
  • Orienté-objet
    • Introduction
    • Membres
    • Extensions de type
    • Classe, structure
    • Interface
    • Expression objet
    • Recommandations
  • 🦚 Aller plus loin
Propulsé par GitBook
Sur cette page
  • Déclaration
  • Type sous-jacent
  • Char
  • Casse
  • Usage
  • Pattern matching
  • Valeurs
  • Flags
  • Combinaisons
  • Enum vs Union
  • Conversion
  • Char enum
  • Extras

Cet article vous a-t-il été utile ?

Modifier sur GitHub
  1. Types composites

Enums

PrécédentUnionsSuivantRecords anonymes

Dernière mise à jour il y a 3 ans

Cet article vous a-t-il été utile ?

Déclaration

Ensemble de constantes nommées dont la valeur est de type entier : → Contrairement au C♯, il faut définir la valeur de tous les membres de l'enum :

type ColorN =
    | Red   = 1
    | Green = 2
    | Blue  = 3

☝️ Noter la différence de syntaxe avec un type union "enum-like" () :

type Color = Red | Green | Blue

Type sous-jacent

  • Contrairement au C♯, il n'y a pas de type sous-jacent par défaut en F♯.

  • Le type sous-jacent est défini au moyen des littéraux définissant les valeurs des membres :

    • 1, 2, 3 → int

    • 1uy, 2uy, 3uy → byte

    • Etc. - cf.

Corolaire : il faut utiliser le même type pour tous les membres, sinon cela ne compile pas !

type ColorN =
    | Red   = 1
    | Green = 2
    | Blue  = 3uy
// 💥 ~~~~~~~~~~~
// Cette expression était censée avoir le type `int` 
// mais elle a ici le type `byte`

Char

Le type char peut être utilisé comme type sous-jacent :

type AnswerChar = Yes='Y' | No='N'

// L'équivalent ne marche pas avec 'string'
type AnswerChar = Yes="Y" | No="N"
// 💥 ~~~~~~~~~~ Littéraux énumérés doivent être de type 'int'...

Casse

Autre différence avec les types union, les membres peuvent être en camelCase :

type File =
    | a = 'a'
    | b = 'b'
    | c = 'c'

Usage

let answerKo = Yes            // 💥 Error FS0039
//             ~~~ La valeur ou le constructeur 'Yes' n'est pas défini.
let answer = AnswerChar.Yes   // 👌 OK

Cast via helpers int et enum (mais pas char) :

let redValue = int ColorN.Red         // enum -> int
let redAgain = enum<ColorN> redValue  // int -> enum via type générique
let red: ColorN = enum redValue       // int -> enum via annotation

// ⚠️ Ne marche pas avec char enum
let ko = char AnswerChar.No   // 💥 Error FS0001
let no: AnswerChar = enum 'N' // 💥 Error FS0001

Pattern matching

type ColorN = Red=1 | Green=2 | Blue=3

let toHex color =
    match color with
    | ColorN.Red   -> "FF0000"
    | ColorN.Green -> "00FF00"
    | ColorN.Blue  -> "0000FF"
    // ⚠️ Warning FS0104: Les enums peuvent accepter des valeurs en dehors des cas connus.
    // Par exemple, la valeur 'enum<ColorN> (0)' peut indiquer un cas non traité...

    // 💡 Pour enlever le warning, il faut ajouter un pattern générique
    | _ -> invalidArg (nameof color) $"Color {color} not supported"

Valeurs

On peut utiliser la méthode System.Enum.GetValues() pour obtenir la liste des membres d'une enum. Par contre, le type de retour est faiblement typé : Array (tableau non générique). → Il suffit d'encapsuler cette méthode dans une fonction helper telle que :

let enumValues<'a> () =
    Enum.GetValues(typeof<'a>)
    :?> ('a array)
    |> Array.toList

let allPermissions = enumValues<PermissionFlags>()
// val allPermissions: PermissionFlags list = [Read; Write; Execute]

💡 Voir également Extras

Flags

Même principe qu'en C♯ où l'on choisit comme valeurs des multiples de 2 afin de pouvoir les combiner :

open System

[<Flags>]
type PermissionFlags =
    | Read    = 1
    | Write   = 2
    | Execute = 4

let permissions = PermissionFlags.Read ||| PermissionFlags.Write
// val permissions: PermissionFlags = Read, Write

let canRead    = permissions.HasFlag PermissionFlags.Read    // true
let canWrite   = permissions.HasFlag PermissionFlags.Write   // true
let canExecute = permissions.HasFlag PermissionFlags.Execute // false

💡 Notes :

  • L'attribut System.FlagsAttribute est facultatif mais permet d'avoir un code plus explicite. En outre, il améliore le rendu des combinaisons de flags : dans l'exemple précédent, la valeur affichée de permissions est Read, Write. Sans l'attribut Flags, cela aurait afficher 3.

  • Opérateur OU binaire ||| (| en C♯) pour combiner des flags

💡 Astuce : utiliser la notation binaire pour la valeur des flags :

[<Flags>]
type PermissionFlags =
    | Read    = 0b001
    | Write   = 0b010
    | Execute = 0b100

Combinaisons

En C♯, l'on peut directement combiner des flags, comme ReadWrite et All ci-dessous :

[Flags]
public enum PermissionFlags 
{
    Read    = 0b001,
    Write   = 0b010,
    Execute = 0b100,

    ReadWrite = Read | Write,
    All = Read | Write | Execute
}

Ce n'est pas possible en F♯ mais 2 alternatives sont possibles :

1) Procéder manuellement aux combinaisons binaires : ce n'est pas dur à faire mais cela nuit à la visibilité car cela demande un peu de réflexion pour retrouver les flags initiaux. On notera aussi que ces combinaisons font partie intégrante de l'enum.

[<Flags>]
type PermissionFlags =
    | Read      = 0b001
    | Write     = 0b010
    | Execute   = 0b100
    | ReadWrite = 0b011 // ❌ Moins explicite que Read | Write
    | All       = 0b111 // ❌ Moins explicite que Read | Write | Execute 

let all = PermissionFlags.All
// val all: PermissionFlags = All

let all' = PermissionFlags.Read ||| PermissionFlags.Write
// val all': PermissionFlags = ReadWrite
// ✔️ Recombinaison automatique du compilateur reconnaissant "All"

2) Utiliser un module compagnon : pour rendre les combinaisons explicites dans le code mais non reconnues / recombinées par le compilateur.

[<System.Flags>]
type Spacing =
    | Left   = 0b0001
    | Right  = 0b0010
    | Top    = 0b0100
    | Bottom = 0b1000

[<RequireQualifiedAccess>]
module Spacing =
    let Horizontal = Spacing.Left ||| Spacing.Right  // ✔️ Human-friendly
    let Vertical = Spacing.Top ||| Spacing.Bottom
    let All = Horizontal ||| Vertical

let horizontal = Spacing.Horizontal
// val horizontal: Spacing = Left, Right
// ❌ Not "Horizontal"

☝ Note : la méthode HasFlag a un comportement différent selon l'option utilisée. C'est mis en valeur dans l'exemple ci-dessous définissant les 2 helpers Enum.values déjà vu plus haut et Enum.flags qui décompose une valeur d'enum en ses flags élémentaires.

[<RequireQualifiedAccess>]
module Enum =
    let values<'enum when 'enum :> System.Enum> =
        System.Enum.GetValues(typeof<'enum>)
        :?> 'enum array
        |> Array.toList

    let flags<'enum when 'enum :> System.Enum> (enumValue: 'enum) =
        values<'enum>
        |> List.filter (enumValue.HasFlag)

let flagsInline = Enum.flags PermissionFlags.All
// val flagsInline: PermissionFlags list = [Read; Write; ReadWrite; Execute; All]
// 👉 Includes "ReadWrite" and "All". Could even include a "None = 0" ❗

let flagsCompanion = Enum.flags Spacing.All
// val flagsCompanion: Spacing list = [Left; Right; Top; Bottom]
// 👍 Only includes core flags

Enum vs Union

Type
Enum
Union

Type sous-jacent

Entières ou char

Quelconques

Qualification

Obligatoire

Qu'en cas de conflit

Matching exhaustif

❌ Non

✅ Oui

PascalCase

✅ Oui

✅ Oui

camelCase

✅ Oui

❌ Non

☝ Recommandation :

  • Préférer une Union dans la majorité des cas

  • Choisir une Enum pour :

    • Interop .NET

    • Besoin de lier des données de type int

Conversion

enum<'enum> : permet de convertir une valeur entière en l'enum 'enum spécifiée

type AnswerNum =
    | Yes = 1
    | No  = 0

let y1 = enum<AnswerNum> 1  // val y1 : AnswerNum = Yes
let y2: AnswerNum = enum 0  // val y2 : AnswerNum = No

int permet la conversion inverse pour récupérer la valeur sous-jacente d'un membre d'une enum

let yesNum = int AnswerNum.Yes  // val yesNum : int = 1

Char enum

Pour les enums dont le type sous-jacent est char, les fonctions enum, int et char ne marchent pas :

type AnswerChar =
    | Yes = 'Y'
    | No  = 'N'

let no_ko: AnswerChar = enum 'N' // 💥 Le type 'int32' ne correspond pas au type 'char'

let y1_ko = int AnswerChar.Yes   // 💥 Le type 'AnswerChar' ne prend pas en charge une conversion vers le type 'int'
let y2_ko = char AnswerChar.Yes  // 💥 Le type 'AnswerChar' ne prend pas en charge une conversion vers le type 'char'

Il faut alors utiliser le module LanguagePrimitives :

let no_ok: AnswerChar = LanguagePrimitives.EnumOfValue 'N' // val no_ok : AnswerChar = No

let y1_ok = LanguagePrimitives.EnumToValue AnswerChar.Yes // val y1_ok : char = 'Y'
let y2_ok = unbox<char> AnswerChar.Yes  // val y2_ok : char = 'Y'

Extras

  • parse<'enum>: string -> 'enum

  • tryParse<'enum>: string -> 'enum option

  • getValues<'enum>: unit -> 'enum seq

#r "nuget: FSharpx.Extras"
open FSharpx

type ColorN =
    | Red   = 1
    | Green = 2
    | Blue  = 3

let red = "Red" |> Enum.tryParse<ColorN> // val red: ColorN option = Some Red
let none = "xx" |> Enum.tryParse<ColorN> // val none: ColorN option = None

let blue: ColorN = "Blue" |> Enum.parse // val blue: ColorN = Blue
let ko =
    try
        "Ko" |> Enum.parse<ColorN>
    with ex ->
        printfn "💥 %s %s" (ex.GetType().FullName) ex.Message
        // 💥 System.ArgumentException: Requested value 'Ko' was not found
        enum<ColorN> 0

let colors = Enum.getValues<ColorN> () |> Seq.toList
// val colors: ColorN list = [Red; Green; Blue]

Contrairement aux unions, l'emploi d'un membre (a.k.a littéral) d'enum est forcément qualifié

Contrairement aux unions, le pattern matching n'est pas exhaustif

Le package NuGet comporte un module Enum proposant ces helpers :

⚠️
⚠️
FSharpx.Extras
Literals
Style "enum"