Spreadsheet

Esempio pratico dello Spreadsheet mentre analizza i dati di un test di usabilità.
I dati inseriti simulano i tempi di completamento (in secondi) e gli errori commessi da 5 utenti che testano una nuova funzionalità (ad esempio, un processo di checkout).
La Media (93s) e la Mediana (90s) dei tempi di completamento sono molto vicine. Questo è un segnale eccellente, perché indica un'esperienza utente consistente e prevedibile, senza valori anomali estremi che distorcerebbero la media.
Gli Errori Totali (5) vengono analizzati insieme ai tempi, rivelando un insight chiave: l'utente con il tempo più lungo (110s) è stato anche quello che ha commesso più errori (3).
In un contesto reale, questo risultato fornisce un chiaro passo successivo: il team UX dovrebbe immediatamente rivedere la registrazione della sessione di quello specifico utente (Utente 3) per identificare il difetto di design che ha causato sia il ritardo sia gli errori, portando a un'ottimizzazione mirata.

Esempio pratico dello Spreadsheet utilizzato per analizzare rapidamente i risultati di un A/B Test.
Questa simulazione confronta due design: "Design A" (il Controllo originale) e "Design B" (la Nuova versione), per vedere quale dei due ha un Tasso di Conversione migliore per il pulsante "Richiedi Demo".
Il foglio di calcolo calcola il Tasso di Conversione per entrambi, mostrando il Design A al 12.4% e il Design B al 15.4%. Questa metrica è cruciale perché misura la forza persuasiva di ciascun design.
La metrica chiave "Lift (Miglioramento)" mostra che il Design B ha una performance di ~3 punti percentuali superiore all'originale.
Questa è una chiara convalida basata sui dati. L'insight che si ottiene da questo spreadsheet è di raccomandare la sostituzione del Design A con il Design B, poiché il nuovo design è palesemente più efficace nel convertire gli utenti.
Il Progetto
Spreadsheet funzionale sviluppato con JavaScript puro, implementando funzioni avanzate come somme, medie, mediane, range e valutazione di formule. Un progetto che dimostra i principi della programmazione funzionale applicati a un caso d'uso reale.
Codice Sorgente
- index.html
- styles.css
- script.js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Functional Programming Spreadsheet</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div id="container">
<div></div>
</div>
</body>
</html>
#container {
display: grid;
grid-template-columns: 50px repeat(10, 200px);
grid-template-rows: repeat(11, 30px);
}
.label {
background-color: lightgray;
text-align: center;
vertical-align: middle;
line-height: 30px;
}
const infixToFunction = {
"+": (x, y) => x + y,
"-": (x, y) => x - y,
"*": (x, y) => x * y,
"/": (x, y) => x / y,
}
const infixEval = (str, regex) => str.replace(regex, (_match, arg1, operator, arg2) => infixToFunction[operator](parseFloat(arg1), parseFloat(arg2)));
const highPrecedence = str => {
const regex = /([\d.]+)([*\/])([\d.]+)/;
const str2 = infixEval(str, regex);
return str === str2 ? str : highPrecedence(str2);
}
const isEven = num => num % 2 === 0;
const sum = nums => nums.reduce((acc, el) => acc + el, 0);
const average = nums => sum(nums) / nums.length;
const median = nums => {
const sorted = nums.slice().sort((a, b) => a - b);
const length = sorted.length;
const middle = length / 2 - 1;
return isEven(length)
? average([sorted[middle], sorted[middle + 1]])
: sorted[Math.ceil(middle)];
}
const spreadsheetFunctions = {
"" : (nums) => nums,
sum,
average,
median,
even: nums => nums.filter(isEven),
someeven: nums => nums.some(isEven),
everyeven: nums => nums.every(isEven),
firsttwo: nums => nums.slice(0, 2),
lasttwo: nums => nums.slice(-2),
has2: nums => nums.includes(2),
increment: nums => nums.map(num => num + 1),
random: ([x, y]) => Math.floor(Math.random() * y + x),
range: nums => range(...nums),
nodupes: nums => [...new Set(nums).values()]
}
const applyFunction = str => {
const noHigh = highPrecedence(str);
const infix = /([\d.]+)([+-])([\d.]+)/;
const str2 = infixEval(noHigh, infix);
const functionCall = /([a-z0-9]*)\(([0-9., ]*)\)(?!.*\()/i;
const toNumberList = args => args.split(",").map(parseFloat);
const apply = (fn, args) => spreadsheetFunctions[fn.toLowerCase()](toNumberList(args));
return str2.replace(functionCall, (match, fn, args) => spreadsheetFunctions.hasOwnProperty(fn.toLowerCase()) ? apply(fn, args) : match);
}
const range = (start, end) => Array(end - start + 1).fill(start).map((element, index) => element + index);
const charRange = (start, end) => range(start.charCodeAt(0), end.charCodeAt(0)).map(code => String.fromCharCode(code));
const evalFormula = (x, cells) => {
const idToText = id => cells.find(cell => cell.id === id).value;
const rangeRegex = /([A-J])([1-9][0-9]?):([A-J])([1-9][0-9]?)/gi;
const rangeFromString = (num1, num2) => range(parseInt(num1), parseInt(num2));
const elemValue = num => character => idToText(character + num);
const addCharacters = character1 => character2 => num => charRange(character1, character2).map(elemValue(num));
const rangeExpanded = x.replace(rangeRegex, (_match, char1, num1, char2, num2) => rangeFromString(num1, num2).map(addCharacters(char1)(char2)));
const cellRegex = /[A-J][1-9][0-9]?/gi;
const cellExpanded = rangeExpanded.replace(cellRegex, match => idToText(match.toUpperCase()));
const functionExpanded = applyFunction(cellExpanded);
return functionExpanded === x ? functionExpanded : evalFormula(functionExpanded, cells);
}
window.onload = () => {
const container = document.getElementById("container");
const createLabel = (name) => {
const label = document.createElement("div");
label.className = "label";
label.textContent = name;
container.appendChild(label);
}
const letters = charRange("A", "J");
letters.forEach(createLabel);
range(1, 99).forEach(number => {
createLabel(number);
letters.forEach(letter => {
const input = document.createElement("input");
input.type = "text";
input.id = letter + number;
input.ariaLabel = letter + number;
input.onchange = update;
container.appendChild(input);
})
})
}
const update = event => {
const element = event.target;
const value = element.value.replace(/\s/g, "");
if (!value.includes(element.id) && value.startsWith('=')) {
element.value = evalFormula(value.slice(1), Array.from(document.getElementById("container").children));
}
}
La Svolta: Quando il Currying ha fatto "Click"
È stato veramente interessante incontrare per la prima volta il currying con funzioni freccia.
All'inizio sembrava solo sintassi strana: character1 => character2 => num => .... Addirittura 3 frecce?
Ora lo reputo un concetto semplicissimo, ma inizialmente non lo era. Perché si è trattato di ristrutturare cognitivamente un concetto che avevo ormai dato per assodato: le arrow function.
Riassumerò qui di seguito tutte le analogie e spiegazioni che mi ha fornito il Code Tutor per farmi comprendere il concetto.
L'Analogia della Fabbrica
Una funzione normale è come un macchinario che ha bisogno di tutte le chiavi inserite contemporaneamente:
const add = (a, b) => a + b; // Devi darle a e b subito
Una funzione curryficata è invece una fabbrica di macchine specializzate:
const curriedAdd = a => b => a + b;
curriedAdd(10): Non ti dà un risultato. Ti dà una macchina nuova con il 10 "saldato" dentro.const add10 = curriedAdd(10): Salvi questa macchina specializzata.add10(5): Ora usi la macchina. Risultato: 15.
Ciò significa che ogni livello "blocca" un pezzo di informazione e passa una nuova funzione al livello successivo. È la closure al lavoro, perciò ogni funzione "si ricorda" (tiene in memoria) i parametri ricevuti ai livelli superiori.
Il File .zip vs La Cartella 📦
Sono andato avanti negli step, ma dopo un errore assolutamente evitabile ho deciso di approfondire ulteriormente: sia perché non volevo rifarne altri, sia perché non volevo lacune.
Ecco l'analogia che per me è stata più efficace:
Versione Lunga (La Cartella 📁):
const elemValue = num => {
const inner = character => idToText(character + num);
return inner;
}
È una cartella. Puoi aprirla, buttarci dentro console.log per il debug, ispezionarla.
Versione Corta (Il File .zip 📦):
const elemValue = num => character => idToText(character + num);
È un file compresso. Contiene esattamente la stessa cosa, ma senza "rumore" (const, return, {}). Mostra quindi solo la logica pura.
A quel punto mi sono chiesto: "La soluzione compatta è certamente più elegante e, paradossalmente, una volta capito il funzionamento è ancora più leggibile della versione estesa. Il prezzo che si paga è la scarsa scalabilità?"
Dopo qualche ricerca, ecco cos'è emerso in termini di "workflow ideale":
- Scrivo la versione corta per eleganza
- Se c'è un bug, "decomprimo" nella versione lunga
- Aggiungo
console.logper il debug - Risolvo il problema
- "Ricompatto" nella versione corta
Cosa Ho Imparato
Programmazione Funzionale Avanzata:
- Currying: Funzioni che restituiscono funzioni, creando specializzazioni progressive.
- Closure: Le funzioni interne tengono in memoria le variabili delle funzioni esterne.
- Partial Application: Bloccare alcuni argomenti per creare nuove funzioni al volo.
- Pure Functions: Funzioni senza effetti collaterali che facilitano debug e testing.
Sintassi Avanzata JavaScript:
- Return implicito con arrow functions:
x => yequivale ax => { return y }. - Currying a più livelli:
a => b => c => result. - Destructuring e pattern matching con regex avanzate.
Manipolazione di Stringhe e Regex:
- Valutazione di formule con regex complesse per operazioni matematiche.
- Pattern
/([A-J])([1-9][0-9]?):([A-J])([1-9][0-9]?)/giper gestire range di celle. - Sostituzione ricorsiva con
.replace()per espandere formule annidate.
Array Methods e Composizione:
.reduce(),.map(),.filter(),.some(),.every(),.slice()in contesti funzionali.- Composizione di funzioni per creare pipeline di trasformazione dati.
new Set()per rimuovere duplicati.
Algoritmi Statistici:
- Implementazione di
sum,average,mediancon approccio funzionale. - Gestione di array pari/dispari per calcolo mediana.
Architettura Spreadsheet:
- Sistema di identificazione celle (A1, B2, etc.).
- Valutazione ricorsiva di formule con dipendenze tra celle.
- Prevenzione di riferimenti circolari.
Riflessione
Ora quando incontro una sintassi come num => character => idToText(character + num), il mio cervello non legge più codice, legge un flusso: "Prendo num, che mi porta a una funzione che prende character, che mi porta al risultato finale."
La sintassi => è diventata per me un diagramma di flusso visuale. Ogni volta che l'ho incontrata successivamente, dicevo dentro di me "porta a...", come se stessi seguendo una mappa. Non più parentesi, return e variabili temporanee da tenere a mente, solo la pura trasformazione dei dati, passo dopo passo.
Prossimo Progetto: Realizzare un validatore di numeri di telefono (Certification Project!)