Lo stato mutabile globale privato è sempre appropriato, ovvero quando viene usato per prevenire l'uso improprio dell'API?

6

Sto scrivendo un controllo di tipo per un dialetto ML che implica la generazione di variabili di tipo "fresche" (nuove e univoche) "(valori che rappresentano le incognite). La mia strategia e la strategia che sembra essere utilizzata nelle esercitazioni è identificare le variabili di tipo con un numero intero univoco e mantenere un contatore incrementato ogni volta che viene generata una nuova variabile di tipo.

In precedenza, ho mantenuto il contatore come un campo mutabile di un tipo di record che rappresenta lo stato del controllo di tipo.

Il programma deve confrontare le variabili di tipo; lo fa confrontando i loro ID interi. Poiché i diversi stati di controllo dei tipi mantengono i propri contatori, è possibile confrontare le variabili di tipo generate da due diversi stati di controllo del tipo e renderle uguali. Questo è un uso improprio dell'API e non dovrebbe mai accadere.

type state =
  { mutable vargen : int
  ; (* Other fields... *) }

let fresh_tvar st =
  st.vargen <- st.vargen + 1;
  Type.Var.{ id = st.vargen - 1
           ; (* Other fields... *) }

let compare lhs rhs = compare lhs.id rhs.id

Per applicare meglio la correttezza, ho deciso di spostare il contatore dallo stato di tipo checker a un membro globale e privato del tipo variable module. Ora, le variabili di tipo distinte non possono mai essere uguali. Il codice al di fuori del modulo non può accedere al contatore o all'ID di una variabile di tipo; possono solo confrontare due variabili di tipo.

module type Var = sig
  type t
  val fresh : some_type -> some_other_type -> t
  val compare : t -> t -> bool
end

module Var : Var = struct
  type t = { id : int; (* Other fields... *) }

  let counter = ref 0 (* Global mutable state not exposed in signature *)

  let fresh some_arg some_other_arg =
    counter := !counter + 1;
    { id = counter - 1
    ; (* Other fields... *) }

  let compare lhs rhs = compare lhs.id rhs.id
end

Poiché lo stato globale mutabile è considerato dannoso (specialmente nella programmazione funzionale!), non sono sicuro che il mio nuovo codice sia valido.

  • Lo stato globale mutabile è giustificato in questo contesto?
  • Se il mio primo progetto è migliore, in generale, come posso far rispettare l'invariante che i valori generati da stati fisicamente separati non vengano usati insieme? (Un altro esempio di tale violazione, in C ++, sarebbe confrontare gli iteratori con contenitori diversi.)
  • Lo stato del controllo di tipo è di per sé un singleton che si comporta come stato globale in tutto tranne il nome?

Sto usando OCaml, a proposito, se gli idiomi specifici della lingua influenzano la risposta alla mia domanda.

    
posta The Aspiring Hacker 23.08.2018 - 03:41
fonte

1 risposta

6

Lo stato globale non è necessariamente malvagio, anche nei linguaggi funzionali. Tuttavia:

  • tutto ciò che fa è applicare una generazione di ID coerente per processo . I processi multipli potrebbero ancora scontrarsi. Devi decidere a che punto l'unicità è abbastanza buona.

  • la generazione di ID globale può rendere più difficile il test. Al contrario, se è possibile utilizzare un nuovo stato di generazione dell'ID per ciascun test, gli ID esatti potrebbero essere riproducibili.

In quasi tutti i programmi ci sarà un elemento dinamico che non puoi rappresentare nel sistema di tipi, qui: solo gli ID generati dalla stessa istanza di controllo di tipo possono essere confrontati. È ragionevole effettuare controlli per rilevare errori accidentali, non è sempre ragionevole mirare a un progetto in cui l'uso improprio non sia possibile. I.e: errare è umano, ma possiamo ancora trattare i consumatori delle API come adulti.

In questo caso, potrebbe essere sensato combinare i due approcci. Fornisci l'incapsulamento utilizzando un modulo, ma supporta anche lo stato esterno per semplificare i test. Per esempio:.

module type Var = sig
  type typevar
  type state
  val newstate : () -> state
  val gensym : state -> some_type -> typevar
  val compare : typevar -> typevar -> bool
end

Se gli ID variabili di tipo riproducibile non sono necessari, quindi mantenere la soluzione con stato globale incapsulato va bene.

Perché lo stato globale incapsulato è discutibilmente buono? La funzione di generazione ID non è chiaramente una funzione pura, poiché dovrebbe restituire un risultato diverso ogni volta. Le funzioni non pure sono comuni, specialmente nelle lingue imperative come OCaml. Questo è simile a qualsiasi funzione che esegue I / O o una funzione che è un generatore di numeri casuali. Ma soprattutto, la funzione di generazione ID ha un flusso di dati molto semplice. Il suo output non dipende dagli input delle funzioni precedenti, ma solo dal numero di invocazioni precedenti.

Ecco un diagramma che illustra il flusso di dati. Le informazioni escono dal sistema incapsulato (sfondo ombreggiato) e gli stati dipendono solo da stati precedenti. Questo stato mutevole è facile da ragionare.

Alcontrario,levariabiliglobali"cattive" consentono ai dati di fluire in più direzioni attraverso il limite del sistema incapsulato. Lo stato del sistema "incapsulato" è condiviso tra tutti gli utenti di quel sistema e l'ID I k dipende da input sconosciuti: possiamo ottenere "azione a distanza" e più utenti (o semplicemente, diverse parti della base di codice) diventano più accoppiati. Questo flusso di dati è molto più difficile da ragionare.

Non tutti gli stati mutabili sono ugualmente cattivi - dovremmo a volte fare un passo indietro e considerare se sia davvero un problema per la nostra applicazione.

    
risposta data 23.08.2018 - 11:18
fonte

Leggi altre domande sui tag