Question

C’est une question assez longue, alors n’hésitez pas à me parler.

Nous implémentons un émulateur pour un composant matériel est développé en même temps. L'idée est de donner à des tiers une solution logicielle pour tester leur logiciel client et donner le matériel Les développeurs ont un point de référence pour implémenter leur firmware.

Les personnes qui ont écrit le protocole pour le matériel ont utilisé un programme personnalisé. version de SUN XDR appelée INCA_XDR. C'est un outil pour sérialiser et désérialiser les messages. C'est écrit en C et nous voulons éviter tout code natif, nous analysons manuellement les données du protocole.

Le protocole est par nature assez complexe et les paquets de données peut avoir de nombreuses structures différentes, mais il a toujours la même structure globale:

  

[HEAD] [INTRO] [DATA] [TAIL]

[HEAD] =
    byte sync 0x03
    byte length X       [MSB]       X = length of [HEADER] + [INTRO] + [DATA]
    byte length X       [LSB]       X = length of [HEADER] + [INTRO] + [DATA]
    byte check X        [MSB]       X = crc of [INTRO] [DATA]
    byte check X        [LSB]       X = crc of [INTRO] [DATA]
    byte headercheck X              X = XOR over [SYNC] [LENGTH] [CHECK]

[INTRO]
    byte version 0x03
    byte address X                  X = 0 for point-to-point, 1-254 for specific controller, 255 = broadcast
    byte sequence X                 X = sequence number
    byte group X        [MSB]       X = The category of the message
    byte group X        [LSB]       X = The category of the message
    byte type X         [MSB]       X = The id of the message
    byte type X         [LSB]       X = The id of the message

[DATA] =
    The actuall data for the specified message,
    this format really differs a lot.

    It always starts with a DRCode which is one byte.
    It more or less specifies the general structure of
    the data, but even within the same structure the data
    can mean many different things and have different lenghts.
    (I think this is an artifact of the INCA_XDR tool)

[TAIL] =
    byte 0x0D

Comme vous pouvez le constater, il y a beaucoup de données de surcharge, mais c'est parce que le protocole doit fonctionner avec RS232 (point à multipoint) et TCP / IP (p2p).

    name        size    value
    drcode      1       1   
    name        8               contains a name that can be used as a file name (only alphanumeric characters allowed)
    timestamp   14              yyyymmddhhmmss  contains timestamp of bitmap library
    size        4               size of bitmap library to be loaded
    options     1               currently no options

Ou il pourrait avoir une structure entièrement différente:

    name        size    value
    drcode      1       2   
    lastblock   1       0 - 1   1 indicates last block. Firmware can be stored
    blocknumber 2               Indicates block of firmware
    blocksize   2       N       size of block to load
    blockdata   N               data of block of firmware

Parfois, il ne s'agit que d'un DRCode et de l'absence de données supplémentaires.

En fonction du groupe et du champ de type, l'émulateur doit effectuer certaines actions. Alors d'abord, nous regardons ceux deux champs et sur la base de ce que nous savons à quoi s'attendre des données et je dois l'analyser correctement.

Ensuite, les données de réponse doivent être générées, ce qui a encore une fois de nombreuses structures de données différentes. Certains messages génèrent simplement un message ACK ou NACK, tandis que d’autres génèrent une vraie réponse avec des données.

Nous avons décidé de diviser les choses en petits morceaux.

Il y a tout d'abord le processeur IData.

Les classes implémentant cette interface sont responsables pour valider des données brutes et générer des instances de la classe Message. Ils ne sont pas responsables de la communication, ils se font simplement passer un octet []

Valider les données brutes signifie vérifier l’en-tête pour rechercher les erreurs de somme de contrôle, crc et de longueur.

Le message résultant est transmis à une classe qui implémente IMessageProcessor. Même si les données brutes étaient considérées comme non valides, car IDataProcessor n’a pas notion de message de réponse ou quoi que ce soit d'autre, il ne fait que valider les données brutes.

Pour informer le processeur IMessageProcessor des erreurs, des propriétés supplémentaires ont été ajoutées. à la classe Message:

bool nakError = false;
bool tailError = false;
bool crcError = false;
bool headerError = false;
bool lengthError = false;

Ils ne sont pas liés au protocole et n'existent que pour le IMessageProcessor

IMessageProcessor est l'endroit où le vrai travail est effectué. En raison de tous les types et groupes de messages différents, j'ai décidé de utilisez F # pour implémenter l'interface IMessageProcessor car la correspondance de modèle semblait être un bon moyen d’éviter de nombreuses déclarations imbriquées if / else et de caste. (Je n'ai aucune expérience préalable de F # ni même de langages fonctionnels autres que LINQ et SQL)

IMessageProcessor analyse les données et décide des méthodes à appeler. sur le IHardwareController. Il peut sembler redondant d’avoir IHardwareController, mais nous voulons pouvoir l'échanger avec une implémentation différente et ne pas être obligé d'utiliser F # non plus. L’implémentation actuelle est une fenêtre WPF, mais cela peut être une fenêtre Cocoa # ou simplement une console par exemple.

Le IHardwareController est également responsable de la gestion de l'état car les développeurs doivent pouvoir manipuler les paramètres matériels et les erreurs via l'interface utilisateur.

Ainsi, une fois qu'IMessageProcessor a appelé les méthodes correctes sur IHardwareController, il doit générer la réponse MEssage. Encore une fois ... les données dans ces messages de réponse peut avoir de nombreuses structures différentes.

Finalement, un IDataFactory est utilisé pour convertir le message en données de protocole brutes prêt à être envoyé à la classe responsable de la communication. (Une encapsulation supplémentaire des données peut être requise, par exemple)

Cela n’est rien "dur". à propos de l'écriture de ce code, mais tous les différents les commandes et les structures de données nécessitent beaucoup de code et il y a peu choses que nous pouvons réutiliser. (Au moins autant que je peux voir maintenant, espérant que quelqu'un pourra me prouver le contraire)

C'est la première fois que j'utilise F #, j'apprends donc au fur et à mesure. Le code ci-dessous est loin d'être terminé et ressemble probablement à un gâchis géant. Il implémente seulement une liste de tous les messages du protocole et je peux vous dire qu'il y en a beaucoup, beaucoup. Donc, ce fichier va devenir énorme!

Important à savoir: l'ordre des octets est inversé sur le fil (raisons historiques)

module Arendee.Hardware.MessageProcessors

open System;
open System.Collections
open Arendee.Hardware.Extenders
open Arendee.Hardware.Interfaces
open System.ComponentModel.Composition
open System.Threading
open System.Text

let VPL_NOERROR = (uint16)0
let VPL_CHECKSUM = (uint16)1
let VPL_FRAMELENGTH = (uint16)2
let VPL_OUTOFSEQUENCE = (uint16)3
let VPL_GROUPNOTSUPPORTED = (uint16)4
let VPL_REQUESTNOTSUPPORTED = (uint16)5
let VPL_EXISTS = (uint16)6
let VPL_INVALID = (uint16)7
let VPL_TYPERROR = (uint16)8
let VPL_NOTLOADING = (uint16)9
let VPL_NOTFOUND = (uint16)10
let VPL_OUTOFMEM = (uint16)11
let VPL_INUSE = (uint16)12
let VPL_SIZE = (uint16)13
let VPL_BUSY = (uint16)14
let SYNC_BYTE = (byte)0xE3
let TAIL_BYTE = (byte)0x0D
let MESSAGE_GROUP_VERSION = 3uy
let MESSAGE_GROUP = 701us


[<Export(typeof<IMessageProcessor>)>]
type public StandardMessageProcessor() = class
    let mutable controller : IHardwareController = null               

    interface IMessageProcessor with
        member this.ProcessMessage m : Message = 
            printfn "%A" controller.Status
            controller.Status <- ControllerStatusExtender.DisableBit(controller.Status,ControllerStatus.Nak)

            match m with
            | m when m.LengthError -> this.nakResponse(m,VPL_FRAMELENGTH)
            | m when m.CrcError -> this.nakResponse(m,VPL_CHECKSUM)
            | m when m.HeaderError -> this.nakResponse(m,VPL_CHECKSUM)
            | m -> this.processValidMessage m
            | _ -> null      

        member public x.HardwareController
            with get () = controller
            and set y = controller <- y                 
    end

    member private this.processValidMessage (m : Message) =
        match m.Intro.MessageGroup with
        | 701us -> this.processDefaultGroupMessage(m);
        | _ -> this.nakResponse(m, VPL_GROUPNOTSUPPORTED);

    member private this.processDefaultGroupMessage(m : Message) =
        match m.Intro.MessageType with
        | (1us) -> this.firmwareVersionListResponse(m)                        //ListFirmwareVersions              0
        | (2us) -> this.StartLoadingFirmwareVersion(m)                     //StartLoadingFirmwareVersion       1
        | (3us) -> this.LoadFirmwareVersionBlock(m)                     //LoadFirmwareVersionBlock          2
        | (4us) -> this.nakResponse(m, VPL_FRAMELENGTH)                       //RemoveFirmwareVersion             3
        | (5us) -> this.nakResponse(m, VPL_FRAMELENGTH)                       //ActivateFirmwareVersion           3        
        | (12us) -> this.nakResponse(m,VPL_FRAMELENGTH)                       //StartLoadingBitmapLibrary         2
        | (13us) -> this.nakResponse(m,VPL_FRAMELENGTH)                       //LoadBitmapLibraryBlock            2        
        | (21us) -> this.nakResponse(m, VPL_FRAMELENGTH)                      //ListFonts                         0
        | (22us) -> this.nakResponse(m, VPL_FRAMELENGTH)                      //LoadFont                          4
        | (23us) -> this.nakResponse(m, VPL_FRAMELENGTH)                      //RemoveFont                        3
        | (24us) -> this.nakResponse(m, VPL_FRAMELENGTH)                      //SetDefaultFont                    3         
        | (31us) -> this.nakResponse(m, VPL_FRAMELENGTH)                      //ListParameterSets                 0
        | (32us) -> this.nakResponse(m, VPL_FRAMELENGTH)                      //LoadParameterSets                 4
        | (33us) -> this.nakResponse(m, VPL_FRAMELENGTH)                      //RemoveParameterSet                3
        | (34us) -> this.nakResponse(m, VPL_FRAMELENGTH)                      //ActivateParameterSet              3
        | (35us) -> this.nakResponse(m, VPL_FRAMELENGTH)                      //GetParameterSet                   3        
        | (41us) -> this.nakResponse(m, VPL_FRAMELENGTH)                      //StartSelfTest                     0
        | (42us) -> this.returnStatus(m)                                      //GetStatus                         0
        | (43us) -> this.nakResponse(m, VPL_FRAMELENGTH)                      //GetStatusDetail                   0
        | (44us) -> this.ResetStatus(m)                     //ResetStatus                       5
        | (45us) -> this.nakResponse(m, VPL_FRAMELENGTH)                      //SetDateTime                       6
        | (46us) -> this.nakResponse(m, VPL_FRAMELENGTH)                      //GetDateTime                       0
        | _ -> this.nakResponse(m, VPL_REQUESTNOTSUPPORTED)



    (* The various responses follow *)

    //Generate a NAK response
    member private this.nakResponse (message : Message , error) =
        controller.Status <- controller.Status ||| ControllerStatus.Nak
        let intro = new MessageIntro()
        intro.MessageGroupVersion <- MESSAGE_GROUP_VERSION
        intro.Address <- message.Intro.Address
        intro.SequenceNumber <- this.setHigh(message.Intro.SequenceNumber)
        intro.MessageGroup <- MESSAGE_GROUP
        intro.MessageType <- 130us
        let errorBytes = UShortExtender.ToIntelOrderedByteArray(error)
        let data = Array.zero_create(5)
        let x = this.getStatusBytes
        let y = this.getStatusBytes
        data.[0] <- 7uy
        data.[1..2] <- this.getStatusBytes
        data.[3..4] <- errorBytes      
        let header = this.buildHeader intro data
        let message = new Message()
        message.Header <- header
        message.Intro <- intro
        message.Tail <- TAIL_BYTE
        message.Data <- data
        message   

    //Generate an ACK response
    member private this.ackResponse (message : Message) =   
        let intro = new MessageIntro()
        intro.MessageGroupVersion <- MESSAGE_GROUP_VERSION
        intro.Address <- message.Intro.Address
        intro.SequenceNumber <- this.setHigh(message.Intro.SequenceNumber)
        intro.MessageGroup <- MESSAGE_GROUP
        intro.MessageType <- 129us
        let data = Array.zero_create(3);
        data.[0] <- 0x05uy
        data.[1..2] <- this.getStatusBytes
        let header = this.buildHeader intro data
        message.Header <- header
        message.Intro <- intro
        message.Tail <- TAIL_BYTE
        message.Data <- data
        message        

    //Generate a ReturnFirmwareVersionList
    member private this.firmwareVersionListResponse (message : Message) =
        //Validation
        if message.Data.[0] <> 0x00uy then
           this.nakResponse(message,VPL_INVALID)
        else
            let intro = new MessageIntro()
            intro.MessageGroupVersion <- MESSAGE_GROUP_VERSION
            intro.Address <- message.Intro.Address
            intro.SequenceNumber <- this.setHigh(message.Intro.SequenceNumber)
            intro.MessageGroup <- MESSAGE_GROUP
            intro.MessageType <- 132us    
            let firmwareVersions = controller.ReturnFirmwareVersionList();
            let firmwareVersionBytes = BitConverter.GetBytes((uint16)firmwareVersions.Count) |> Array.rev

            //Create the data
            let data = Array.zero_create(3 + (int)firmwareVersions.Count * 27)
            data.[0] <- 0x09uy                              //drcode
            data.[1..2] <- firmwareVersionBytes             //Number of firmware versions

            let mutable index = 0
            let loops = firmwareVersions.Count - 1
            for i = 0 to loops do
                let nameBytes = ASCIIEncoding.ASCII.GetBytes(firmwareVersions.[i].Name) |>  Array.rev
                let timestampBytes = this.getTimeStampBytes firmwareVersions.[i].Timestamp |> Array.rev
                let sizeBytes = BitConverter.GetBytes(firmwareVersions.[i].Size) |> Array.rev

                data.[index + 3 .. index + 10] <- nameBytes
                data.[index + 11 .. index + 24] <- timestampBytes
                data.[index + 25 .. index + 28] <- sizeBytes
                data.[index + 29] <- firmwareVersions.[i].Status
                index <- index + 27            

            let header = this.buildHeader intro data
            message.Header <- header
            message.Intro <- intro
            message.Data <- data
            message.Tail <- TAIL_BYTE
            message

    //Generate ReturnStatus
    member private this.returnStatus (message : Message) =
        //Validation
        if message.Data.[0] <> 0x00uy then
           this.nakResponse(message,VPL_INVALID)
        else
            let intro = new MessageIntro()
            intro.MessageGroupVersion <- MESSAGE_GROUP_VERSION
            intro.Address <- message.Intro.Address
            intro.SequenceNumber <- this.setHigh(message.Intro.SequenceNumber)
            intro.MessageGroup <- MESSAGE_GROUP
            intro.MessageType <- 131us

            let statusDetails = controller.ReturnStatus();

            let sizeBytes = BitConverter.GetBytes((uint16)statusDetails.Length) |> Array.rev

            let detailBytes = ASCIIEncoding.ASCII.GetBytes(statusDetails) |> Array.rev

            let data = Array.zero_create(statusDetails.Length + 5)
            data.[0] <- 0x08uy
            data.[1..2] <- this.getStatusBytes
            data.[3..4] <- sizeBytes    //Details size
            data.[5..5 + statusDetails.Length - 1] <- detailBytes

            let header = this.buildHeader intro data
            message.Header <- header
            message.Intro <- intro
            message.Data <- data
            message.Tail <- TAIL_BYTE
            message

    //Reset some status bytes    
    member private this.ResetStatus (message : Message) =
        if message.Data.[0] <> 0x05uy then
            this.nakResponse(message, VPL_INVALID)
        else        
            let flagBytes = message.Data.[1..2] |> Array.rev 
            let flags = Enum.ToObject(typeof<ControllerStatus>,BitConverter.ToInt16(flagBytes,0)) :?> ControllerStatus
            let retVal = controller.ResetStatus flags

            if retVal <> 0x00us then
                this.nakResponse(message,retVal)
            else
                this.ackResponse(message)

    //StartLoadingFirmwareVersion (Ack/Nak)
    member private this.StartLoadingFirmwareVersion (message : Message) =
        if (message.Data.[0] <> 0x01uy) then
            this.nakResponse(message, VPL_INVALID)
        else
            //Analyze the data
            let name = message.Data.[1..8] |> Array.rev |> ASCIIEncoding.ASCII.GetString
            let text = message.Data.[9..22] |> Array.rev |> Seq.map(fun x -> ASCIIEncoding.ASCII.GetBytes(x.ToString()).[0]) |> Seq.to_array |> ASCIIEncoding.ASCII.GetString
            let timestamp = DateTime.ParseExact(text,"yyyyMMddHHmmss",Thread.CurrentThread.CurrentCulture)

            let size = BitConverter.ToUInt32(message.Data.[23..26] |> Array.rev,0)
            let overwrite = 
                match message.Data.[27] with
                | 0x00uy -> false
                | _ -> true

            //Create a FirmwareVersion instance
            let firmware = new FirmwareVersion();
            firmware.Name <- name
            firmware.Timestamp <- timestamp
            firmware.Size <- size

            let retVal = controller.StartLoadingFirmwareVersion(firmware,overwrite)

            if retVal <> 0x00us then
                this.nakResponse(message, retVal) //The controller denied the request
            else
                this.ackResponse(message);

    //LoadFirmwareVersionBlock (ACK/NAK)
    member private this.LoadFirmwareVersionBlock (message : Message) =
        if message.Data.[0] <> 0x02uy then
            this.nakResponse(message, VPL_INVALID)
        else
            //Analyze the data
            let lastBlock = 
                match message.Data.[1] with
                | 0x00uy -> false
                | _true -> true

            let blockNumber = BitConverter.ToUInt16(message.Data.[2..3] |> Array.rev,0)            
            let blockSize = BitConverter.ToUInt16(message.Data.[4..5] |> Array.rev,0)
            let blockData = message.Data.[6..6 + (int)blockSize - 1] |> Array.rev

            let retVal = controller.LoadFirmwareVersionBlock(lastBlock, blockNumber, blockSize, blockData)

            if retVal <> 0x00us then
                this.nakResponse(message, retVal)
            else
                this.ackResponse(message)


    (* Helper methods *)
    //We need to convert the DateTime instance to a byte[] understood by the device "yyyymmddhhmmss"
    member private this.getTimeStampBytes (date : DateTime) =
        let stringNumberToByte s = Byte.Parse(s.ToString()) //Casting to (byte) would give different results

        let yearString = date.Year.ToString("0000")
        let monthString = date.Month.ToString("00")
        let dayString = date.Day.ToString("00")
        let hourString = date.Hour.ToString("00")
        let minuteString = date.Minute.ToString("00")
        let secondsString = date.Second.ToString("00")

        let y1 = stringNumberToByte yearString.[0]
        let y2 = stringNumberToByte yearString.[1]
        let y3 = stringNumberToByte yearString.[2]
        let y4 = stringNumberToByte yearString.[3]  
        let m1 = stringNumberToByte monthString.[0]
        let m2 = stringNumberToByte monthString.[1]
        let d1 = stringNumberToByte dayString.[0]
        let d2 = stringNumberToByte dayString.[1]
        let h1 = stringNumberToByte hourString.[0]
        let h2 = stringNumberToByte hourString.[1]
        let min1 = stringNumberToByte minuteString.[0]
        let min2 = stringNumberToByte minuteString.[1]
        let s1 = stringNumberToByte secondsString.[0]
        let s2 = stringNumberToByte secondsString.[1]

        [| y1 ; y2 ; y3 ; y4 ; m1 ; m2 ; d1 ; d2 ; h1 ; h2 ; min1 ; min2 ; s1; s2 |]

    //Sets the high bit of a byte to 1
    member private this.setHigh (b : byte) : byte = 
        let array = new BitArray([| b |])
        array.[7] <- true
        let mutable converted = [| 0 |]
        array.CopyTo(converted, 0);
        (byte)converted.[0]

    //Build the header of a Message based on Intro + Data
    member private this.buildHeader (intro : MessageIntro) (data : byte[]) =
        let headerLength = 7;
        let introLength = 7;
        let length = (uint16)(headerLength + introLength + data.Length)
        let crcData = ByteArrayExtender.Concat(intro.GetRawData(),data)
        let crcValue = ByteArrayExtender.CalculateCRC16(crcData)
        let lengthBytes = UShortExtender.ToIntelOrderedByteArray(length);
        let crcValueBytes = UShortExtender.ToIntelOrderedByteArray(crcValue);
        let headerChecksum = (byte)(SYNC_BYTE ^^^ lengthBytes.[0] ^^^ lengthBytes.[1] ^^^ crcValueBytes.[0] ^^^ crcValueBytes.[1])
        let header = new MessageHeader();
        header.Sync <- SYNC_BYTE
        header.Length <- length
        header.HeaderChecksum <- headerChecksum
        header.DataChecksum <- crcValue
        header

    member private this.getStatusBytes =
        let l = controller.Status
        let status = (uint16)controller.Status
        let statusBytes = BitConverter.GetBytes(status);
        statusBytes |> Array.rev

end

(Veuillez noter que dans le code source réel, les classes ont des noms différents, plus spécifiques que "Matériel")

J'espère des suggestions, des façons d'améliorer le code ou même différentes manières de gérer le problème. Par exemple, l’utilisation d’un langage dynamique tel que IronPython faciliterait-elle les choses, Est-ce que je vais dans le mauvais sens tous ensemble. Quelle est votre expérience avec des problèmes comme celui-ci, que changeriez-vous, éviterez-vous, etc ....

Mise à jour:

Sur la base de la réponse de Brian, j’ai écrit ce qui suit:

type DrCode9Item = {Name : string ; Timestamp : DateTime ; Size : uint32; Status : byte}
type DrCode11Item = {Id : byte ; X : uint16 ; Y : uint16 ; SizeX : uint16 ; SizeY : uint16
                     Font : string ; Alignment : byte ; Scroll : byte ; Flash : byte}
type DrCode12Item = {Id : byte ; X : uint16 ; Y : uint16 ; SizeX : uint16 ; SizeY : uint16}
type DrCode14Item = {X : byte ; Y : byte}

type DRType =
| DrCode0 of byte
| DrCode1 of byte * string * DateTime * uint32 * byte
| DrCode2 of byte * byte * uint16 * uint16 * array<byte>
| DrCode3 of byte * string
| DrCode4 of byte * string * DateTime * byte * uint16 * array<byte>
| DrCode5 of byte * uint16
| DrCode6 of byte * DateTime
| DrCode7 of byte * uint16 * uint16
| DrCode8 of byte * uint16 * uint16 * uint16 * array<byte>
| DrCode9 of byte * uint16 * array<DrCode9Item>
| DrCode10 of byte * string * DateTime * uint32 * byte * array<byte>
| DrCode11 of byte * array<DrCode11Item>
| DrCode12 of byte * array<DrCode12Item>
| DrCode13 of byte * uint16 * byte * uint16 * uint16 * string * byte * byte
| DrCode14 of byte * array<DrCode14Item>

Je pourrais continuer à le faire pour tous les types de reprise après sinistre (plusieurs), mais je ne comprends toujours pas comment cela pourrait m'aider. j'ai lu sur Wikibooks et dans Foundations of F # mais quelque chose ne clique pas encore dans ma tête.

Mise à jour 2

Donc, je comprends que je pourrais faire ce qui suit:

let execute dr =
    match dr with
    | DrCode0(drCode) -> printfn "Do something"
    | DrCode1(drCode, name, timestamp, size, options) -> printfn "Show the size %A" size
    | _ -> ()
let date = DateTime.Now

let x = DrCode1(1uy,"blabla", date, 100ul, 0uy)

Mais quand le message arrive dans IMessageProcessor, le choix est fait ici quel genre de message il est et la fonction appropriée est alors appelée. Ce qui précède serait juste être du code supplémentaire, du moins c'est comment le comprendre, donc je dois vraiment manquer le point ici ... mais je ne le vois pas.

execute x
Était-ce utile?

La solution

Je pense que F # est un choix naturel pour représenter les messages dans ce domaine via des syndicats discriminés; J'imagine, par exemple.

type Message =
    | Message1 of string * DateTime * int * byte //name,timestamp,size,options
    | Message2 of bool * short * short * byte[]  //last,blocknum,blocksize,data
    ...

avec les méthodes pour analyser / décompresser les messages de / vers un tableau d'octets. Comme vous le dites, ce travail est simple, fastidieux.

Je suis moins clair sur le traitement des messages, mais globalement, selon votre description, vous avez l’impression que vous en avez la maîtrise.

Je suis un peu préoccupé par votre "flexibilité des outils" - quelles sont vos contraintes? (par exemple .Net, doit être mis à jour par des programmeurs qui connaissent les technologies X, Y, Z, doivent répondre à certains critères de performance, ...)

Autres conseils

Voici mes 2 centimes (mise en garde: je ne connais pas de F #): vous avez un fichier d’entrée finement spécifié, même avec une grammaire complète. Vous souhaitez mapper le contenu du fichier à des actions. Par conséquent, je vous suggère d'analyser le fichier. F # étant un langage fonctionnel, il peut s’adapter à la technique d’analyse appelée Analyse récursive de descente . = result & resnum = 1 # PPA475, M1 "rel =" nofollow noreferrer "> Le livre" Expert F # " contient une analyse de l'analyse récursive par descente.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top