2016-02-06 14 views
9

Contesto: Mi sto avvicinando a Haskell dal punto di vista della conversione degli errori di runtime in errori di compilazione. La mia ipotesi è che questo è possibile se si può codificare la logica di business all'interno dei tipi del programma stesso.Codifica presenza/assenza di autenticazione a livello di tipo

Sto scrivendo un bot di Telegram, che dovrebbe essere accessibile dagli utenti all'interno della mia azienda. Per ottenere questa "restrizione", ogni volta che qualcuno inizia a chattare con il bot, cerca in chat_id una tabella e controlla se esiste uno oauth_token valido. In caso contrario, all'utente verrà prima inviato un link per completare un OAuth di Google (l'e-mail della nostra azienda è ospitata su Google Apps for Business).

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| 
VLUser 
    email String 
    chatId Integer 
    tgramUserId Integer 
    tgramFirstName String 
    tgramLastName String Maybe 
    tgramUsername String Maybe 
    oauthToken String Maybe 
    deriving Show 
|] 

Gli utenti con una valida oauth_token sarà in grado di dare il bot Telegram alcuni comandi, che gli utenti non autenticati non dovrebbero essere in grado di dare.

Ora, sto cercando di codificare questa logica a livello di tipo stesso. Ci saranno alcune funzioni nel mio codice Haskell che avranno la possibilità di accettare, come argomenti, entrambi gli utenti autentificati non autentificati, &; mentre alcune funzioni dovrebbero accettare solo utenti autenticati.

Se continuo a passare oggetti utente dello stesso tipo, vale a dire VLUser ovunque, quindi dovrò fare attenzione a verificare la presenza di oauthToken in ogni funzione. C'è un modo per creare due tipi di utente - VLUser e VLUserAuthenticated dove:

  1. Sia mappa per la stessa tabella sottostante
  2. Un VLUserAuthenticated può essere un'istanza solo se si ha un oauthToken
+0

Questo sembra simile al problema di un mutex. Si desidera specificare a livello di testo se la funzione richiede il blocco o meno. Puoi vedere un possibile modo di affrontare questo [qui] (http://dev.stephendiehl.com/hask/#indexed-monads) per vedere il tipo 'IState'. Sostituisci 'Locked' e' Unlocked' con 'Authenticated' e' UnAuthenticated' e fornisci all'utente una funzione 'login' che controlla il token' oauth' e che è l'unico modo per ottenere un valore 'Authenticated'. – Bakuriu

risposta

8

Phantom types to the rescue! è Bryan O'Sullivan esempio di implementazione di read-only vs accesso in lettura/scrittura al livello di tipo utilizzando phantom types.

Allo stesso modo, per il vostro caso d'uso:

data Unknown  -- unknown users 
data Authenticated -- verified users 

newtype User a i = Id i deriving Show 

È importante che il costruttore di dati Id è non esposto a utente, ma il modulo fornisce funzioni per inizializzare e autenticare gli utenti:

-- initializes an un-authenticated user 
newUser :: i -> User Unknown i 
newUser = Id 

-- authenticates a user 
authUser :: (User a i) -> User Authenticated i 
authUser (Id i) = Id i -- dummy implementation 

quindi, è possibile controllare l'accesso al livello di tipo senza duplicazione codice, senza controlli runtime e senza runtime costo:

-- open to all users 
getId :: User a i -> i 
getId (Id i) = i 

-- only authenticated users can pass through 
getId' :: User Authenticated i -> i 
getId' (Id i) = i 

Ad esempio, se

\> let jim = newUser "jim" 
\> let joe = authUser $ newUser "joe" 

joe è un utente autenticato e può essere passato a due funzioni:

\> getId joe 
"joe" 
\> getId' joe 
"joe" 

mentre, si otterrà errore in fase di compilazione se si chiama getId' con jim:

\> getId jim 
"jim" 
\> getId' jim -- compile-time error! not run-time error! 

<interactive>:28:8: 
    Couldn't match type ‘Unknown’ with ‘Authenticated’ 
    Expected type: User Authenticated [Char] 
     Actual type: User Unknown [Char] 
    In the first argument of ‘getId'’, namely ‘jim’ 
    In the expression: getId' jim 
+0

Mentre questa sembra una soluzione praticabile, è questo l'unico approccio? Sembra solo un gran numero di piastre di caldaia per qualcosa che può essere facilmente risolto con l'ereditarietà in altre lingue. –

+0

Se questa piastra è necessaria, esiste una macro che può convertire questo in un unico rivestimento? –

+0

@SaurabhNanda non sono d'accordo! in realtà è il contrario: nessun codice per eseguire controlli del tempo di esecuzione e nessun codice duplicato per implementare la stessa logica tra i tipi di utenti –