38

Quali sarebbero le buone strutture di dati puramente funzionali per gli editor di testo? Voglio essere in grado di inserire singoli caratteri nel testo ed eliminare singoli caratteri dal testo con efficienza accettabile, e mi piacerebbe poter mantenere le vecchie versioni, così posso annullare le modifiche con facilità.Strutture dati puramente funzionali per editor di testo

Devo utilizzare solo un elenco di stringhe e riutilizzare le righe che non cambiano da versione a versione?

+14

si può guardare in cerniere .. – Satvik

+3

Potreste essere costretti dal framework GUI che si sta utilizzando (a meno che non hai intenzione di un'applicazione basata console) che potrebbero gestire tutto il montaggio del testo per te direttamente dalla scatola. – AndrewC

+1

Cerca la lista con cerniera. –

risposta

10

A Vector[Vector[Char]] probabilmente sarebbe una buona scommessa. È un IndexedSeq quindi ha prestazioni decenti di aggiornamento/pre-aggiornamento/aggiornamento, a differenza dello List che hai citato. Se guardi allo Performance Characteristics, è l'unica collezione immutabile menzionata che ha un aggiornamento costante in tempo reale.

+2

L'ho provato, e funziona benissimo! – fredoverflow

1

Le possibilità che vengono in mente sono:

  1. Il tipo "testo" con un indice numerico. Mantiene il testo in un elenco collegato di buffer (la rappresentazione interna è UTF16), quindi mentre in teoria la sua complessità computazionale è solitamente quella di una lista collegata (ad esempio l'indicizzazione è O (n)), in pratica è molto più veloce di una convenzionale collegata elenco che probabilmente potresti semplicemente dimenticare l'impatto di n a meno che non stia memorizzando l'intera Wikipedia nel tuo buffer. Prova alcuni esperimenti su un testo da 1 milione di caratteri per vedere se ho ragione (qualcosa che non ho ancora fatto, BTW).

  2. Una cerniera testo: memorizzare il testo dopo il cursore in un elemento di testo, e il testo prima il cursore in un altro. Per spostare il cursore trasferisci il testo da un lato all'altro.

53

Non so se questo suggerimento è "buono" per le definizioni sofisticate di "buono", ma è facile e divertente. Ho spesso impostato un esercizio per scrivere il nucleo di un editor di testo in Haskell, collegandolo con il codice di rendering che fornisco. Il modello dati è il seguente.

Innanzitutto, definire ciò che deve essere un cursore all'interno di un elenco di x -elementi, in cui le informazioni disponibili al cursore contiene qualche tipo m. (Il x si rivelerà essere Char o String.)

type Cursor x m = (Bwd x, m, [x]) 

Questa Bwd cosa è solo il arretrate "SNOC-liste". Voglio mantenere forti intuizioni spaziali, quindi cambio le cose nel mio codice, non nella mia testa. L'idea è che la roba più vicina al cursore sia la più facilmente accessibile. Questo è lo spirito di The Zipper.

data Bwd x = B0 | Bwd x :< x deriving (Show, Eq) 

fornisco un tipo Singleton gratuita di agire come marcatore leggibile per il cursore ...

data Here = Here deriving Show 

... e posso quindi dire che cosa è di essere da qualche parte in un String

type StringCursor = Cursor Char Here 

Ora, per rappresentare un buffer di più linee, dobbiamo String s sopra e sotto la linea con il cursore, ed una StringCursor nel mezzo, per la linea che' attualmente in fase di modifica.

type TextCursor = Cursor String StringCursor 

Questo tipo TextCursor è tutto che uso per rappresentare lo stato del buffer di modifica. È una cerniera a due strati. Fornisco agli studenti codice per rendere una finestra sul testo in una finestra di shell abilitata per ANSI-escape, assicurando che il viewport contenga il cursore. Tutto quello che devono fare è implementare il codice che aggiorna lo TextCursor in risposta alle sequenze di tasti.

handleKey :: Key -> TextCursor -> Maybe (Damage, TextCursor) 

dove handleKey dovrebbe restituire Nothing se il tasto è priva di senso, ma per il resto fornire Just un aggiornamento TextCursor e un "rapporto danni", quest'ultimo è uno dei

data Damage 
    = NoChange  -- use this if nothing at all happened 
    | PointChanged -- use this if you moved the cursor but kept the text 
    | LineChanged -- use this if you changed text only on the current line 
    | LotsChanged -- use this if you changed text off the current line 
    deriving (Show, Eq, Ord) 

(Se vi state chiedendo qual è la differenza tra restituire Nothing e restituire Just (NoChange, ...), considerare se si desidera che l'editor venga emesso un segnale acustico.) Il rapporto sui danni indica al renderer quanto lavoro deve svolgere per aggiornare l'immagine visualizzata.

Il tipo Key fornisce semplicemente una rappresentazione di dataype leggibile alle possibili sequenze di tasti, allontanandosi dalle sequenze di escape ANSI non elaborate. È insignificante.

ho fornire agli studenti un grande indizio per andare su e giù con questo modello di dati, offrendo questi pezzi di corredo:

deactivate :: Cursor x Here -> (Int, [x]) 
deactivate c = outward 0 c where 
    outward i (B0, Here, xs)  = (i, xs) 
    outward i (xz :< x, Here, xs) = outward (i + 1) (xz, Here, x : xs) 

La funzione deactivate viene utilizzata per spostare l'attenzione da un Cursor, dando una lista ordinaria, ma che ti dice dove il cursore era. La funzione corrispondente activate tenta di posizionare il cursore in una determinata posizione in un elenco:

activate :: (Int, [x]) -> Cursor x Here 
activate (i, xs) = inward i (B0, Here, xs) where 
    inward _ [email protected](_, Here, [])  = c -- we can go no further 
    inward 0 c     = c -- we should go no further 
    inward i (xz, Here, x : xs) = inward (i - 1) (xz :< x, Here, xs) -- and on! 

offro agli studenti una definizione volutamente scorretta e incompleta di handleKey

handleKey :: Key -> TextCursor -> Maybe (Damage, TextCursor) 
handleKey (CharKey c) (sz, 
         (cz, Here, cs), 
         ss) 
    = Just (LineChanged, (sz, 
         (cz, Here, c : cs), 
         ss)) 
handleKey _ _ = Nothing 

che gestisce solo le battiture carattere ordinario, ma fa uscire il testo all'indietro. È facile vedere che il carattere c appare a destra di Here. Li invito a correggere il bug e aggiungere funzionalità per i tasti freccia, backspace, delete, return e così via.

Potrebbe non essere la rappresentazione più efficiente di sempre, ma è puramente funzionale e consente al codice di conformarsi concretamente alle nostre intuizioni spaziali sul testo che viene modificato.

+0

Il codice può essere attualmente ottenuto tramite 'darcs clone http: // personal.cis.strath.ac.uk/conor.mcbride/CS410', secondo http://www.reddit.com/r/haskell/comments/11pwh6 /. – nobody

2

La comunità Clojure sta guardando RRB Trees (Relaxed Radix Bilanciato) come strcuture persistente dei dati per i vettori di dati che possono essere concatenati in modo efficiente/fette/inserita ecc

Permette concatenazione, insert-at-indice e dividere le operazioni in tempo O (log N).

Immagino che un albero RRB specializzato per i dati dei caratteri sia perfettamente adatto per grandi strutture di dati di testo "modificabili".

4

Suggerirei di usare zippers in combinazione con Data.Sequence.Seq che è basato su finger trees. Così si potrebbe rappresentare lo stato attuale come

data Cursor = Cursor { upLines :: Seq Line 
        , curLine :: CurLine 
        , downLines :: Seq Line } 

Questo vi dà O (1) complessità per spostare il cursore su/giù una singola linea, e dal momento che splitAt e (><) (unione) hanno entrambi O (log (min (n1, n2))) complessità, riceverai O (log (L)) complessità per saltare L righe su/giù.

Si potrebbe avere una struttura di cerniera simile per CurLine per mantenere una sequenza di caratteri prima, dopo e dopo il cursore.

Line potrebbe essere qualcosa di efficiente in termini di spazio, ad esempio ByteString.

Problemi correlati