In primo luogo, un disclaimer: non ho una profonda familiarità con il principio di accesso uniforme, quindi potresti voler prendere questo con un pizzico di sale. Detto questo, direi che Clojure osserva un principio di accesso uniforme: chiamate di funzione.
La citazione chiave su Wikipedia sembra essere che "tutti i servizi offerti da un modulo dovrebbero essere disponibili attraverso una notazione uniforme, che non tradisce se sono implementati attraverso lo storage o attraverso il calcolo", e questo è esattamente il caso con le chiamate di funzione in Clojure. Infatti, tutto in Clojure (e altri Lisps) è una chiamata di funzione ad eccezione di moduli speciali e macro - e le macro in realtà sono funzioni, con la differenza che operano sul codice sorgente. Puoi anche specificare il comportamento delle chiamate di funzione per i tuoi tipi implementando clojure.lang.IFn
:
(deftype Invokable []
clojure.lang.IFn
(invoke [this]
:was-invoked))
(def invokable (Invokable.))
(invokable) ;=> :was-invoked
Potresti avere in mente l'uso di parole chiave come funzioni, che sono più specificamente associate con le hash-map, ma puoi effettivamente implementare quel comportamento anche per tipi diversi. Ecco un esempio stupido in cui l'accesso a parole chiave legge un file (con lo stesso nome della parola chiave) dalla tua home directory o restituisce un valore predefinito se tale file non esiste:
(ns weird.example
(:require [clojure.java.io :as io]))
(deftype WeirdKlass []
clojure.lang.ILookup
(valAt [this k not-found]
(let [home (System/getProperty "user.home")
file (io/file home (name k))]
(if (.isFile file)
(slurp file)
not-found)))
(valAt [this k]
(.valAt this k nil)))
(def klass (WeirdKlass.))
;; assuming you have a file at $HOME/testfile
(:testfile klass) ;=> "test content!"
(:blah klass) ;=> nil
(:blah klass :not-found) ;=> :not-found
Aggiornamento basato sul commento di Alexey
Penso di avere un'idea migliore di ciò che intendi ora ma in pratica non ho trovato che fosse un inconveniente. Quando vuoi o hai bisogno di un livello di riferimento / astrazione tra la funzionalità del tuo codice e la rappresentazione dei dati sottostanti, il modo per ottenerlo è: funzioni!
Il modo più semplice sarebbe definire full-name
come una funzione che capita di utilizzare la ricerca di parole chiave, ma che potrebbe essere successivamente modificata per calcolare il nome (senza che i clienti ne sappiano o si preoccupino):
;; For now
(defn full-name [person]
(:full-name person))
;; Maybe later
(defn full-name [person]
(str (:first-name person)
" "
(:last-name person)))
Naturalmente, puoi anche utilizzare record e protocolli:
(defprotocol IPerson
(full-name [p])
(first-name [p])
(last-name [p]))
(defrecord Person1 [name]
IPerson
(full-name [_]
name)
(first-name [_]
(first (.split name " ")))
(last-name [_]
(last (.split name " "))))
(def person1 (->Person1 "Joe Schmoe"))
(full-name person1) ;=> "Joe Schmoe"
(first-name person1) ;=> "Joe"
(last-name person1) ;=> "Schmoe"
(defrecord Person2 [f-name l-name]
IPerson
(full-name [_]
(str f-name " " l-name))
(first-name [_]
f-name)
(last-name [_]
l-name))
(def person2 (->Person2 "Joe" "Schmoe"))
(full-name person2) ;=> "Joe Schmoe"
(first-name person2) ;=> "Joe"
(last-name person2) ;=> "Schmoe"
Il lato grande ( molto grande, a mio avviso) dell'usare strutture di dati "semplici" quando possibile è che significa che voi e i vostri utenti avete a disposizione la gamma completa di operazioni di mappa e sequenza di Clojure . Alan Perlis ha espresso bene questa idea quando ha affermato che " è meglio avere 100 funzioni operative su una struttura dati di 10 funzioni su 10 strutture di dati . "