2015-05-02 37 views
9

Stavo cercando di codificare un problema relazionale in Haskell, quando ho dovuto scoprire che fare questo in un modo sicuro è tutt'altro che ovvio. Per esempio. un umilePerché le operazioni relazionali per la sicurezza dei caratteri sono così difficili?

select 1,a,b, from T 

solleva già una serie di domande:

  • qual è il tipo di questa funzione?
  • qual è il tipo di proiezione 1,a,b? Qual è il tipo di a proiezione in generale?
  • qual è il tipo di risultato e come posso esprimere la relazione tra il tipo di risultato e la proiezione?
  • qual è il tipo di tale funzione che accetta qualsiasi proiezione valida?
  • come è possibile rilevare le proiezioni non valide al momento della compilazione?
  • Come aggiungere una colonna a un tavolo oa una proiezione?

Credo che persino il linguaggio PL/SQL di Oracle non lo giustamente. Mentre le proiezioni invald vengono per lo più rilevate in fase di compilazione, si tratta di un numero elevato di errori di tipo che vengono visualizzati solo in fase di esecuzione. La maggior parte degli altri binding a RDBMS (ad esempio jdbc di Java e DBI di perl) utilizzano SQL contenuto in String e quindi rinuncia completamente alla sicurezza del tipo.

Ulteriori ricerche hanno dimostrato che ci sono alcune librerie Haskell (HList, vinyl e TRex), che forniscono type-safe record estensibili e un po 'di più. Ma tutte queste librerie richiedono estensioni Haskell come DataKinds, FlexibleContexts e molte altre ancora. Inoltre queste librerie non sono facili da usare e hanno un odore di inganno, almeno per osservatori non inizializzati come me.

Ciò suggerisce che le operazioni relazionali basate sul tipo non si adattano bene al paradigma funzionale, almeno non come è implementato in Haskell.

Le mie domande sono le seguenti:

  • Quali sono le cause fondamentali di questa difficoltà per modellare le operazioni relazionali in un tipo modo sicuro. Dove finisce Hindley-Milner? O il problema si origina già al calcolo lambda tipizzato?
  • Esiste un paradigma, in cui le operazioni relazionali sono cittadini di prima classe? E se è così, c'è un'implementazione nel mondo reale?
+1

Hai esaminato le librerie che consentono l'accesso al DB di tipo sicuro in Haskell? Per esempio. https://hackage.haskell.org/package/esqueleto o http://www.yesodweb.com/book/persistent? – chi

+0

Non ancora. L'accesso al database non è la mia preoccupazione principale. Ma quei pacchetti devono aver risolto i miei problemi in un modo o nell'altro, quindi potrei dare un'occhiata. Grazie. –

risposta

4

Definiamo una tabella indicizzata su alcune colonne come un tipo con due parametri di tipo:

data IndexedTable k v = ??? 

groupBy :: (v -> k) -> IndexedTable k v 

-- A table without an index just has an empty key 
type Table = IndexedTable() 

k sarà un (eventualmente inserita) tupla di tutte le colonne che la tabella è indicizzato su. v sarà una tupla (possibilmente annidata) di tutte le colonne su cui la tabella non è indicizzata.

Così, per esempio, se avessimo la seguente tabella

| Id | First Name | Last Name | 
|----|------------|-----------| 
| 0 | Gabriel | Gonzalez | 
| 1 | Oscar  | Boykin | 
| 2 | Edgar  | Codd  | 

...e fosse indicizzato sulla prima colonna, quindi il tipo sarebbe:

type Id = Int 
type FirstName = String 
type LastName = String 

IndexedTable Int (FirstName, LastName) 

Tuttavia, se fosse indicizzato sulla prima e seconda colonna, quindi il tipo sarebbe:

IndexedTable (Int, Firstname) LastName 

Table sarebbe implementare le classi di tipi Functor, Applicative e Alternative. In altre parole:

instance Functor (IndexedTable k) 

instance Applicative (IndexedTable k) 

instance Alternative (IndexedTable k) 

Così si unisce verrebbe implementata come:

join :: IndexedTable k v1 -> IndexedTable k v2 -> IndexedTable k (v1, v2) 
join t1 t2 = liftA2 (,) t1 t2 

leftJoin :: IndexedTable k v1 -> IndexedTable k v2 -> IndexedTable k (v1, Maybe v2) 
leftJoin t1 t2 = liftA2 (,) t1 (optional t2) 

rightJoin :: IndexedTable k v1 -> IndexedTable k v2 -> IndexedTable k (Maybe v1, v2) 
rightJoin t1 t2 = liftA2 (,) (optional t1) t2 

allora si avrebbe un tipo distinto che chiameremo un Select. Questo tipo avrà anche due parametri di tipo:

data Select v r = ??? 

Un Select consumerebbe un gruppo di file di tipo v dal tavolo e produrre un risultato di tipo r. In altre parole, si dovrebbe avere una funzione di tipo:

selectIndexed :: Indexed k v -> Select v r -> r 

Qualche esempio Select s che potremmo definire sarebbero:

count :: Select v Integer 
sum  :: Num a => Select a a 
product :: Num a => Select a a 
max  :: Ord a => Select a a 

Questo tipo Select sarebbe implementare l'interfaccia Applicative, quindi si potrebbe combinare più Select s in un singolo Select. Per esempio:

liftA2 (,) count sum :: Select Integer (Integer, Integer) 

Quello sarebbe analogo a questo SQL:

SELECT COUNT(*), SUM(*) 

Tuttavia, spesso la nostra tabella avrà più colonne, quindi abbiamo bisogno di un modo per mettere a fuoco un Select su una singola colonna. Chiamiamo questa funzione Focus:

focus :: Lens' a b -> Select b r -> Select a r 

In modo che possiamo scrivere cose come:

liftA3 (,,) (focus _1 sum) (focus _2 product) (focus _3 max) 
    :: (Num a, Num b, Ord c) 
    => Select (a, b, c) (a, b, c) 

Quindi, se abbiamo voluto scrivere qualcosa di simile:

SELECT COUNT(*), MAX(firstName) FROM t 

Questo sarebbe equivalente a questo Codice Haskell:

firstName :: Lens' Row String 

table :: Table Row 

select table (liftA2 (,) count (focus firstName max)) :: (Integer, String) 

Quindi potresti chiederti come si potrebbe implementare Select e Table.

descrivo come implementare Table in questo post:

http://www.haskellforall.com/2014/12/a-very-general-api-for-relational-joins.html

...e si può implementare Select come solo:

type Select = Control.Foldl.Fold 

type focus = Control.Foldl.pretraverse 

-- Assuming you define a `Foldable` instance for `IndexedTable` 
select t s = Control.Foldl.fold s t 

Inoltre, tenere a mente che questi non sono gli unici modi per implementare Table e Select. Sono solo una semplice implementazione per iniziare e puoi generalizzarli come necessario.

Che ne dici di selezionare le colonne da una tabella? Beh, è ​​possibile definire:

column :: Select a (Table a) 
column = Control.Foldl.list 

Quindi, se si voleva fare:

SELECT col FROM t 

... si scrive:

field :: Lens' Row Field 

table :: Table Row 

select (focus field column) table :: [Field] 

Il takeaway importante è che è possibile implementare un API relazionale in Haskell bene senza estensioni di sistema di tipo fantasioso.

+1

Trovo che due cose confondano: (1) nel tuo ragionamento i _keys_ svolgono un ruolo terribilmente importante. Credo che le chiavi siano necessarie per determinate garanzie, ma non per le operazioni relazionali in quanto tali. (2) Il tipo di risultato di _select_ non dovrebbe essere nuovamente una tabella? –

+0

Ho aggiornato il mio post. (1) Per una tabella in cui non ti interessa il tasto, imposta semplicemente il tipo di chiave su '()'. (2) Puoi recuperare una tabella semplicemente applicando una selezione appropriata che restituisce una tabella. Un 'Select' può restituire una lista/tabella di risultati. –