Qual è il vantaggio del curry?

148

Ho appena saputo di curry e, mentre penso di aver capito il concetto, non vedo alcun grosso vantaggio nell'usarlo.

Come esempio banale uso una funzione che aggiunge due valori (scritti in ML). La versione senza curriculum sarebbe

fun add(x, y) = x + y

e verrebbe chiamato come

add(3, 5)

mentre la versione al curry è

fun add x y = x + y 
(* short for val add = fn x => fn y=> x + y *)

e verrebbe chiamato come

add 3 5

Mi sembra che sia solo zucchero sintattico che rimuove un insieme di parentesi dalla definizione e dal richiamo della funzione. Ho visto il curriculum elencato come una delle caratteristiche importanti di un linguaggio funzionale, e al momento sono un po 'deludente. Il concetto di creare una catena di funzioni che consumano ogni singolo parametro, invece di una funzione che richiede una tupla, sembra piuttosto complicato da utilizzare per un semplice cambiamento di sintassi.

La sintassi un po 'più semplice è l'unica motivazione per il curriculum, o mi mancano altri vantaggi che non sono ovvi nel mio esempio molto semplice? Sta valutando solo zucchero sintattico?

    
posta Mad Scientist 01.02.2013 - 20:36
fonte

15 risposte

123

Con le funzioni al curry puoi ottenere un più facile riutilizzo di più funzioni astratte, dal momento che ti specializzi. Supponiamo che tu abbia una funzione di aggiunta

add x y = x + y

e che vuoi aggiungere 2 a ogni membro di una lista. In Haskell dovresti fare questo:

map (add 2) [1, 2, 3] -- gives [3, 4, 5]
-- actually one could just do: map (2+) [1, 2, 3], but that may be Haskell specific

Qui la sintassi è più leggera rispetto a quando dovessi creare una funzione add2

add2 y = add 2 y
map add2 [1, 2, 3]

o se dovessi creare una funzione lambda anonima:

map (\y -> 2 + y) [1, 2, 3]

Ti consente anche di astrarre le diverse implementazioni. Supponiamo che tu abbia due funzioni di ricerca. Uno da un elenco di coppie chiave / valore e una chiave per un valore e un altro da una mappa da chiavi a valori e una chiave per un valore, come questo:

lookup1 :: [(Key, Value)] -> Key -> Value -- or perhaps it should be Maybe Value
lookup2 :: Map Key Value -> Key -> Value

Quindi potresti creare una funzione che accetta una funzione di ricerca da Chiave a valore. Potresti passare a una delle funzioni di ricerca sopra elencate, parzialmente applicate rispettivamente con una lista o una mappa:

myFunc :: (Key -> Value) -> .....

In conclusione: il currying è buono, perché ti consente di specializzare / applicare parzialmente le funzioni usando una sintassi leggera e quindi passare queste funzioni parzialmente applicate a una funzione di ordine superiore come map o filter . Funzioni di ordine superiore (che assumono funzioni come parametri o le producono come risultati) sono il pane e il burro della programmazione funzionale, e le funzioni di curry e parzialmente applicate consentono di utilizzare funzioni di ordine superiore in modo molto più efficace e conciso.

    
risposta data 01.02.2013 - 21:23
fonte
53

La risposta pratica è che il curring semplifica molto la creazione di funzioni anonime. Anche con una sintassi lambda minima, è una sorta di vittoria; confrontare:

map (add 1) [1..10]
map (\ x -> add 1 x) [1..10]

Se hai una brutta sintassi lambda, è anche peggio. (Ti sto guardando, JavaScript, Scheme e Python.)

Questo diventa sempre più utile quando usi sempre più funzioni di ordine superiore. Mentre uso le più funzioni di ordine superiore in Haskell rispetto ad altre lingue, ho trovato che effettivamente uso la sintassi lambda less perché qualcosa come i due terzi del tempo, lambda sarebbe solo una funzione parzialmente applicata. (E molto altro tempo l'ho estratto in una funzione con nome.)

Più in generale, non è sempre ovvio quale versione di una funzione sia "canonica". Ad esempio, prendi map . Il tipo di map può essere scritto in due modi:

map :: (a -> b) -> [a] -> [b]
map :: (a -> b) -> ([a] -> [b])

Quale è quella "corretta"? In realtà è difficile da dire. In pratica, la maggior parte delle lingue usa la prima: la mappa prende una funzione e una lista e restituisce una lista. Tuttavia, fondamentalmente, ciò che la mappa effettivamente fa è mappare le normali funzioni per elencare le funzioni: prende una funzione e restituisce una funzione. Se la mappa è al curry, non devi rispondere a questa domanda: lo fa entrambi , in un modo molto elegante.

Questo diventa particolarmente importante una volta generalizzato map in tipi diversi da elenco.

Inoltre, il curry in realtà non è molto complicato. In realtà è un po 'una semplificazione rispetto al modello utilizzato dalla maggior parte delle lingue: non è necessario alcun concetto di funzioni di più argomenti cotti nella tua lingua. Questo riflette anche il calcolo lambda sottostante più da vicino.

Ovviamente, i linguaggi in stile ML non hanno una nozione di più argomenti in forma al curry o in forma non sollecitata. La sintassi f(a, b, c) corrisponde in realtà al passaggio della tupla (a, b, c) in f , quindi f assume solo un argomento. Questa è in realtà una distinzione molto utile che vorrei che altre lingue avrebbero perché rende molto naturale scrivere qualcosa del tipo:

map f [(1,2,3), (4,5,6), (7, 8, 9)]

Non è possibile farlo facilmente con le lingue che hanno l'idea di più argomenti inseriti direttamente in!

    
risposta data 01.02.2013 - 21:31
fonte
21

Il currying può essere utile se hai una funzione che stai passando in giro come oggetto di prima classe, e non ricevi tutti i parametri necessari per valutarla in un posto nel codice. Puoi semplicemente applicare uno o più parametri quando li ottieni e passare il risultato a un altro pezzo di codice che ha più parametri e finire di valutarlo lì.

Il codice per ottenere questo risultato sarà più semplice di prima se è necessario ottenere tutti i parametri insieme.

Inoltre, c'è la possibilità di riutilizzare più codice, dal momento che le funzioni che utilizzano un singolo parametro (un'altra funzione al curry) non devono corrispondere come specificamente con tutti i parametri.

    
risposta data 01.02.2013 - 21:07
fonte
14

La motivazione principale (almeno inizialmente) per il curring non era pratica ma teorica. In particolare, il curring consente di ottenere in modo efficace funzioni a più argomenti senza effettivamente definire la semantica per loro o definire la semantica per i prodotti. Ciò porta a un linguaggio più semplice con la stessa espressività di un altro, un linguaggio più complicato, e quindi è auspicabile.

    
risposta data 02.02.2013 - 15:12
fonte
14

(Darò degli esempi in Haskell.)

  1. Quando si usano i linguaggi funzionali è molto conveniente che si possa applicare parzialmente una funzione. Come in% co_de di Haskell è una funzione che restituisce (== x) se il suo argomento è uguale a un dato termine True :

    mem :: Eq a => a -> [a] -> Bool
    mem x lst = any (== x) lst
    

    senza curriculum, avremmo un codice un po 'meno leggibile:

    mem x lst = any (\y -> y == x) lst
    
  2. Questo è legato alla programmazione Tacit (vedi anche Stile senza puntatori su Haskell wiki). Questo stile non si concentra sui valori rappresentati dalle variabili, ma sulla composizione delle funzioni e sul modo in cui le informazioni fluiscono attraverso una catena di funzioni. Possiamo convertire il nostro esempio in un modulo che non utilizza affatto le variabili:

    mem = any . (==)
    

    Qui vediamo x come una funzione da == a a e a -> Bool come una funzione da any a a -> Bool . Semplicemente componendoli, otteniamo il risultato. Questo è tutto grazie al curry.

  3. Il contrario, non-currying, è anche utile in alcune situazioni. Ad esempio, supponiamo di voler dividere una lista in due parti: elementi inferiori a 10 e il resto, quindi concatenare questi due elenchi. La suddivisione dell'elenco è effettuata da [a] -> Bool partition (qui usiamo anche% curried% co_de). Il risultato è di tipo (< 10) . Invece di estrarre il risultato nella sua prima e seconda parte e combinarli usando < , possiamo farlo direttamente non percorrendo ([Int],[Int]) come

    uncurry (++) . partition (< 10)
    

Infatti, ++ valuta ++ .

Ci sono anche importanti vantaggi teorici:

  1. Il currying è essenziale per le lingue che mancano di tipi di dati e hanno solo funzioni, come il calcolo lambda . Sebbene queste lingue non siano utili per l'uso pratico, sono molto importanti da un punto di vista teorico.
  2. Questo è connesso con la proprietà essenziale dei linguaggi funzionali - le funzioni sono oggetti di prima classe. Come abbiamo visto, la conversione da (uncurry (++) . partition (< 10)) [4,12,11,1] a [4,1,12,11] significa che il risultato di quest'ultima funzione è di tipo (a, b) -> c . In altre parole, il risultato è una funzione.
  3. (Un) currying è strettamente connesso a categorie chiuse cartesiane , che è un modo categorico di visualizzare i calcoli lambda digitati.
risposta data 01.02.2013 - 22:14
fonte
9

Il curry non è solo zucchero sintattico!

Considera le firme del tipo di add1 (non affrettato) e add2 (al curry):

add1 : (int * int) -> int
add2 : int -> (int -> int)

(In entrambi i casi, le parentesi nella firma del tipo sono opzionali, ma le ho incluse per chiarezza.)

add1 è una funzione che accetta una 2-tupla di int e int e restituisce un int . add2 è una funzione che accetta int e restituisce un'altra funzione che a sua volta prende un int e restituisce un int .

La differenza essenziale tra i due diventa più visibile quando specifichiamo esplicitamente l'applicazione della funzione. Definiamo una funzione (non curvata) che applica il suo primo argomento al suo secondo argomento:

apply(f, b) = f b

Ora possiamo vedere più chiaramente la differenza tra add1 e add2 . add1 viene chiamato con una tupla di 2:

apply(add1, (3, 5))

ma add2 viene chiamato con int e quindi il suo valore di ritorno viene chiamato con un altro int :

apply(apply(add2, 3), 5)

EDIT: Il vantaggio essenziale del currying è che ottieni un'applicazione parziale gratuitamente. Diciamo che volevi una funzione di tipo int -> int (diciamo, a map su una lista) che ha aggiunto 5 al suo parametro. Potresti scrivere addFiveToParam x = x+5 , o potresti fare l'equivalente con un lambda in linea, ma potresti anche farlo molto più facilmente (specialmente nei casi meno banali di questo) scrivi add2 5 !

    
risposta data 01.02.2013 - 20:51
fonte
9

Il curry è solo zucchero sintattico, ma tu stai leggermente fraintendendo quello che fa lo zucchero, penso. Prendendo il tuo esempio,

fun add x y = x + y

è in realtà zucchero sintattico per

fun add x = fn y => x + y

Cioè, (aggiungi x) restituisce una funzione che prende un argomento ye aggiunge x a y.

fun addTuple (x, y) = x + y

Questa è una funzione che prende una tupla e aggiunge i suoi elementi. Queste due funzioni sono in realtà abbastanza diverse; prendono argomenti diversi.

Se si desidera aggiungere 2 a tutti i numeri in un elenco:

(* add 2 to all numbers using the uncurried function *)
map (fn x => addTuple (x, 2)) [1,2,3]
(* using the curried function *)
map (add 2) [1,2,3]

Il risultato sarebbe [3,4,5] .

Se si desidera sommare ciascuna tupla in una lista, d'altra parte, la funzione addTuple si adatta perfettamente.

(* Sum each tuple using the uncurried function *)
map addTuple [(10,2), (10,3), (10,4)]    
(* sum each tuple using curried function *)
map (fn (a,b) => add a b) [(10,2), (10,3), (10,4)]

Il risultato sarebbe [12,13,14] .

Le funzioni al curry sono grandi se è utile un'applicazione parziale, ad esempio mappa, piega, app, filtro. Considera questa funzione, che restituisce il numero più alto positivo nell'elenco fornito, o 0 se non ci sono numeri positivi:

- val highestPositive = foldr Int.max 0;   
val highestPositive = fn : int list -> int 
    
risposta data 02.02.2013 - 15:43
fonte
9

Un'altra cosa che non ho ancora visto menzionato è che il curring consente un'astrazione (limitata) sull'arità.

Considera queste funzioni che fanno parte della libreria di Haskell

(.) :: (b -> c) -> (a -> b) -> a -> c
either :: (a -> c) -> (b -> c) -> Either a b -> c
flip :: (a -> b -> c) -> b -> a -> c
on :: (b -> b -> c) -> (a -> b) -> a -> a -> c

In ogni caso la variabile del tipo c può essere un tipo di funzione in modo che queste funzioni funzionino su un prefisso dell'elenco dei parametri dell'argomento. Senza curriculum, avresti bisogno di una funzione linguistica speciale per astrarre la funzione arity o avere diverse versioni di queste funzioni specializzate per le diverse competenze.

    
risposta data 03.02.2013 - 04:01
fonte
6

La mia comprensione limitata è tale:

1) Applicazione di funzioni parziali

Applicazione di funzioni parziali è il processo di restituzione di una funzione che richiede un numero minore di argomenti. Se fornisci 2 argomenti su 3, restituirà una funzione che richiede 3-2 = 1 argomento. Se fornisci 1 argomento su 3, restituirà una funzione che richiede 3-1 = 2 argomenti. Se lo volevi, potresti anche applicare parzialmente 3 argomenti su 3 e restituirebbe una funzione che non accetta argomenti.

Quindi data la seguente funzione:

f(x,y,z) = x + y + z;

Quando vincoli da 1 a x e in parte lo applichi alla funzione precedente f(x,y,z) otterrai:

f(1,y,z) = f'(y,z);

Dove: f'(y,z) = 1 + y + z;

Ora se dovessi associare y a 2 e z a 3 e applicare parzialmente f'(y,z) otterrai:

f'(2,3) = f''();

Dove: f''() = 1 + 2 + 3 ;

Ora, in qualsiasi momento, puoi scegliere di valutare f , f' o f'' . Quindi posso fare:

print(f''()) // and it would return 6;

o

print(f'(1,1)) // and it would return 3;

2) Currying

Currying d'altra parte è il processo di divisione di una funzione in una catena nidificata di funzioni di un argomento. Non puoi mai fornire più di 1 argomento, è uno o zero.

Quindi ha la stessa funzione:

f(x,y,z) = x + y + z;

Se lo curasti, otterrai una catena di 3 funzioni:

f'(x) -> f''(y) -> f'''(z)

Dove:

f'(x) = x + f''(y);

f''(y) = y + f'''(z);

f'''(z) = z;

Ora se chiami f'(x) con x = 1 :

f'(1) = 1 + f''(y);

Viene restituita una nuova funzione:

g(y) = 1 + f''(y);

Se chiami g(y) con y = 2 :

g(2) = 1 + 2 + f'''(z);

Viene restituita una nuova funzione:

h(z) = 1 + 2 + f'''(z);

Infine, se chiami h(z) con z = 3 :

h(3) = 1 + 2 + 3;

Sei restituito 6 .

3) Chiusura

Infine, Closure è il processo di acquisizione di una funzione e di dati insieme come una singola unità. Una chiusura di funzione può richiedere da 0 a un numero infinito di argomenti, ma è anche a conoscenza dei dati non passati ad esso.

Di nuovo, data la stessa funzione:

f(x,y,z) = x + y + z;

Puoi invece scrivere una chiusura:

f(x) = x + f'(y, z);

Dove:

f'(y,z) = x + y + z;

f' è chiuso su x . Significa che f' può leggere il valore di x che è all'interno di f .

Quindi se dovessi chiamare f con x = 1 :

f(1) = 1 + f'(y, z);

Avresti una chiusura:

closureOfF(y, z) =
                   var x = 1;
                   f'(y, z);

Ora se hai chiamato closureOfF con y = 2 e z = 3 :

closureOfF(2, 3) = 
                   var x = 1;
                   x + 2 + 3;

Quale restituirebbe 6

Conclusione

Il currying, l'applicazione parziale e le chiusure sono in qualche modo simili in quanto decompongono una funzione in più parti.

Currying decompone una funzione di più argomenti in funzioni annidate di singoli argomenti che restituiscono funzioni di singoli argomenti. Non ha senso inserire una funzione di uno o meno argomento, poiché non ha senso.

L'applicazione parziale decompone una funzione di più argomenti in una funzione di argomenti minori i cui argomenti ora mancanti sono stati sostituiti per il valore fornito.

Closure decompone una funzione in una funzione e un set di dati in cui le variabili all'interno della funzione che non sono state passate possono guardare all'interno del set di dati per trovare un valore da associare quando viene richiesto di valutare.

Ciò che è confuso da tutto ciò è che possono essere usati per implementare un sottoinsieme degli altri. Quindi, in sostanza, sono tutti un dettaglio di implementazione. Offrono tutti un valore simile in quanto non è necessario raccogliere tutti i valori in anticipo e in quanto è possibile riutilizzare parte della funzione, poiché è stata decomposta in unità discrete.

La divulgazione

Non sono affatto un esperto dell'argomento, solo di recente ho iniziato a conoscerli e quindi fornisco le mie conoscenze attuali, ma potrebbero avere degli errori che ti invito a sottolineare, e correggerò come / se ne scopro.

    
risposta data 16.04.2015 - 07:44
fonte
5

Il Currying (applicazione parziale) consente di creare una nuova funzione da una funzione esistente fissando alcuni parametri. È un caso speciale di chiusura lessicale in cui la funzione anonima è solo un involucro banale che passa alcuni argomenti acquisiti ad un'altra funzione. Possiamo anche farlo usando la sintassi generale per fare chiusure lessicali, ma un'applicazione parziale fornisce uno zucchero sintattico semplificato.

Questo è il motivo per cui i programmatori Lisp, quando lavorano in uno stile funzionale, a volte usano librerie per l'applicazione parziale .

Invece di (lambda (x) (+ 3 x)) , che ci dà una funzione che aggiunge 3 al suo argomento, puoi scrivere qualcosa come (op + 3) , e quindi aggiungere 3 a ogni elemento di una lista sarebbe quindi (mapcar (op + 3) some-list) piuttosto che %codice%. Questa macro (mapcar (lambda (x) (+ 3 x)) some-list) ti renderà una funzione che accetta alcuni argomenti op e invoca x y z ... .

In molti linguaggi puramente funzionali, l'applicazione parziale è radicata nella sintassi in modo tale che non vi sia alcun operatore (+ a x y z ...) . Per attivare un'applicazione parziale, è sufficiente chiamare una funzione con un numero di argomenti inferiore a quello necessario. Invece di produrre un errore op , il risultato è una funzione degli argomenti rimanenti.

    
risposta data 01.02.2013 - 22:48
fonte
4

Per la funzione

fun add(x, y) = x + y

È della forma f': 'a * 'b -> 'c

Per valutare uno farà

add(3, 5)
val it = 8 : int

Per la funzione al curry

fun add x y = x + y

Per valutare uno farà

add 3
val it = fn : int -> int

Dove si tratta di un calcolo parziale, in particolare (3 + y), che quindi si può completare il calcolo con

it 5
val it = 8 : int

aggiungi nel secondo caso è nella forma f: 'a -> 'b -> 'c

Ciò che sta facendo qui sta trasformando una funzione che prende due accordi in uno che richiede solo il ritorno di un risultato. Valutazione parziale

Why would one need this?

Dire x su RHS non è solo un normale int, ma piuttosto un calcolo complesso che richiede un po 'di tempo per completare, per aumenti, sake, due secondi.

x = twoSecondsComputation(z)

Quindi la funzione ora sembra

fun add (z:int) (y:int) : int =
    let
        val x = twoSecondsComputation(z)
    in
        x + y
    end;

Di tipo add : int * int -> int

Ora vogliamo calcolare questa funzione per un intervallo di numeri, mappiamola

val result1 = map (fn x => add (20, x)) [3, 5, 7];

Per quanto sopra il risultato di twoSecondsComputation viene valutato ogni volta. Ciò significa che ci vogliono 6 secondi per questo calcolo.

Usando una combinazione di stadiazione e curry, si può evitare questo.

fun add (z:int) : int -> int =
    let
        val x = twoSecondsComputation(z)
    in
        (fn y => x + y)
    end;

della forma al curry add : int -> int -> int

Ora si può fare,

val add' = add 20;
val result2 = map add' [3, 5, 7, 11, 13];

Il twoSecondsComputation deve essere valutato solo una volta. Per aumentare la scala, sostituisci due secondi con 15 minuti o qualsiasi ora, quindi disponi di una mappa contro 100 numeri.

Sommario : il currying è ottimo quando si utilizzano altri metodi per funzioni di livello superiore come strumento di valutazione parziale. Il suo scopo non può essere dimostrato da solo.

    
risposta data 02.02.2013 - 16:22
fonte
3

Currying consente una composizione flessibile delle funzioni.

Ho inventato una funzione "curry". In questo contesto, non mi interessa che tipo di logger ottengo o da dove viene. Non mi interessa quale sia l'azione o da dove viene. Tutto quello che mi interessa è elaborare il mio input.

var builder = curry(function(input, logger, action) {
     logger.log("Starting action");
     try {
         action(input);
         logger.log("Success!");
     }
     catch (err) {
         logger.logerror("Boo we failed..", err);
     }
});
var x = "My input.";
goGatherArgs(builder)(x); // Supplies action first, then logger somewhere.

La variabile builder è una funzione che restituisce una funzione che restituisce una funzione che prende il mio input che fa il mio lavoro. Questo è un semplice esempio utile e non un oggetto in vista.

    
risposta data 01.02.2013 - 21:53
fonte
2

La conversione è un vantaggio quando non si hanno tutti gli argomenti per una funzione. Se ti capita di valutare completamente la funzione, allora non c'è alcuna differenza significativa.

Currying ti consente di evitare di menzionare parametri non ancora necessari. È più conciso e non richiede la ricerca di un nome di parametro che non entri in collisione con un'altra variabile nell'ambito (che è il mio vantaggio preferito).

Ad esempio, quando si usano funzioni che assumono funzioni come argomenti, ti troverai spesso in situazioni in cui hai bisogno di funzioni come "aggiungi 3 all'input" o "confronta input alla variabile v". Con il currying, queste funzioni possono essere facilmente scritte: add 3 e (== v) . Senza curry, devi usare le espressioni lambda: x => add 3 x e x => x == v . Le espressioni lambda sono due volte più lunghe e hanno una piccola quantità di lavoro impegnato correlato alla scelta di un nome oltre a x se c'è già un x in ambito.

Un vantaggio collaterale delle lingue basate sul curriculum è che, quando si scrive codice generico per funzioni, non si ottengono centinaia di varianti in base al numero di parametri. Ad esempio, in C #, un metodo 'curry' avrebbe bisogno di varianti per Func < R >, Func < A, R & gt ;, Func < A1, A2, R & gt ;, Func < A1, A2, A3, R & gt ;, e così via per sempre. In Haskell, l'equivalente di Func < A1, A2, R > è più simile a Func < Tuple < A1, A2 >, R > o un Func < A1, Func < A2, R > > (e Func < R > è più simile a Func < Unit, R >), quindi tutte le varianti corrispondono al singolo Func < A, R > caso.

    
risposta data 01.02.2013 - 23:39
fonte
2

Il ragionamento principale a cui riesco a pensare (e non sono un esperto su questo argomento con qualsiasi mezzo) inizia a mostrare i suoi benefici in quanto le funzioni passano da banali a non banali. In tutti i casi banali con la maggior parte dei concetti di questa natura non troverai alcun beneficio reale. Tuttavia, la maggior parte dei linguaggi funzionali fa un uso pesante dello stack nelle operazioni di elaborazione. Considera PostScript o Lisp come esempi di questo. Usando il currying, le funzioni possono essere impilate in modo più efficace e questo vantaggio diventa evidente mentre le operazioni diventano sempre meno banali. Nel modo curried, il comando e gli argomenti possono essere lanciati in pila in ordine e saltati all'occorrenza in modo che vengano eseguiti nell'ordine corretto.

    
risposta data 09.11.2013 - 11:13
fonte
0

Il currying dipende in modo cruciale (in modo definitivo) dalla capacità di restituire una funzione.

Considera questo pseudo-codice (inventato).

var f = (m, x, b) = > ... restituisci qualcosa ...

Stabiliamo che chiamare f con meno di tre argomenti restituisca una funzione.

var g = f (0, 1); // restituisce una funzione associata a 0 e 1 (m e x) che accetta un altro argomento (b).

var y = g (42); // invoca g con il terzo argomento mancante, usando 0 e 1 per me x

Che tu possa parzialmente applicare gli argomenti e recuperare una funzione riutilizzabile (legata a quegli argomenti che hai fornito) è abbastanza utile (e ASCIUTTA).

    
risposta data 06.07.2014 - 18:27
fonte

Leggi altre domande sui tag