Functional Programming Intro: Eliza Chatbot Without Classes
To show equivalency of FP and OOP let's rebuild an Eliza chatbot using functional programming principles. Instead of objects with methods, we'll use pure functions, immutable data, and composition.
Interestingly React pushes these concepts, but you can do these ideas just fine using vanilla JS with Web Components as our view layer.
Core FP Tenets
- Pure Functions: Same input → same output, no side effects
- Immutable Data: Never modify, always create new
- Composition: Build complex behavior from simple functions
- First-class Functions: Functions as values to pass around
1. Pure Functions vs Methods
OOP Approach
class MessageProcessor {
  constructor() {
    this.patterns = [...];
  }
  process(input) {
    this.lastInput = input; // side effect
    return this.patterns.find(p => p.test(input));
  }
}
FP Approach
// Pure function - no hidden state
const findMatchingPattern = (patterns, input) =>
  patterns.find(pattern => pattern.regex.test(input));
// Data and behavior separate
const patterns = [
  { regex: /hello/i, responses: ['Hi there!', 'Hello!'] },
  { regex: /bye/i, responses: ['Goodbye!', 'See you!'] }
];
const match = findMatchingPattern(patterns, 'hello'); // predictable
Key insight: React components work this way - props => UI. Same props always render same output.
2. Immutable State Updates
The Problem
// Mutable - dangerous!
const addMessage = (messages, text) => {
  messages.push({ text, timestamp: Date.now() });
  return messages; // same array reference
};
FP Solution
// Immutable - create new array
const addMessage = (messages, text) => [
  ...messages,
  { text, timestamp: Date.now(), id: crypto.randomUUID() }
];
// Usage
let conversation = [];
conversation = addMessage(conversation, 'Hello'); // reassign to new array
conversation = addMessage(conversation, 'How are you?');
Why this matters: Makes state changes traceable. React's useState hook forces this pattern.
3. Function Composition
Build complex transformations by chaining simple functions.
// Small, focused functions
const normalize = str => str.trim().toLowerCase();
const tokenize = str => str.split(/\s+/);
const removeStopWords = tokens =>
  tokens.filter(t => !['a', 'the', 'is'].includes(t));
// Compose them
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const processInput = pipe(
  normalize,
  tokenize,
  removeStopWords
);
processInput('  The QUICK brown  '); // ['quick', 'brown']
Real use: Transform user input → extract keywords → match patterns → generate response.
4. Higher-Order Functions for Response Selection
Functions that take or return functions.
// HOF: Returns a function customized by score strategy
const createMatcher = (scoreStrategy) => (patterns, input) => {
  const scored = patterns.map(pattern => ({
    pattern,
    score: scoreStrategy(pattern, input)
  }));
  return scored.sort((a, b) => b.score - a.score)[0].pattern;
};
// Different scoring strategies
const keywordScore = (pattern, input) =>
  pattern.keywords.filter(k => input.includes(k)).length;
const regexScore = (pattern, input) =>
  pattern.regex.test(input) ? 10 : 0;
// Create specialized matchers
const keywordMatcher = createMatcher(keywordScore);
const regexMatcher = createMatcher(regexScore);
React parallel: Custom hooks are HOFs that return stateful functions.
5. Managing Side Effects (The Real World)
FP doesn't eliminate side effects - it isolates them. Fundamentally, most applications require side effects—how else will you do output!
// Pure core logic
const generateResponse = (pattern) =>
  pattern.responses[Math.floor(Math.random() * pattern.responses.length)];
const createConversationTurn = (messages, userInput, botResponse) => ({
  messages: [
    ...messages,
    { role: 'user', text: userInput },
    { role: 'bot', text: botResponse }
  ],
  timestamp: Date.now()
});
// Impure shell - handles DOM, async, etc.
const handleUserMessage = (state) => (inputText) => {
  // Pure transformations
  const pattern = findMatchingPattern(patterns, inputText);
  const response = generateResponse(pattern);
  const newState = createConversationTurn(
    state.messages,
    inputText,
    response
  );
  // Side effect: Update DOM (isolated)
  render(newState);
  return newState;
};
Pattern: Pure core + impure shell. React does this: render logic is pure, side effects are within useEffect.
6. Functional Web Component
// Pure render function
const renderMessage = (msg) => `
  <div class="message message--${msg.role}">
    <p>${msg.text}</p>
    <time>${new Date(msg.timestamp).toLocaleTimeString()}</time>
  </div>
`;
const renderConversation = (messages) =>
  messages.map(renderMessage).join('');
// Web Component as thin wrapper
class ChatView extends HTMLElement {
  connectedCallback() {
    this.render();
  }
  set messages(msgs) {
    this._messages = msgs;
    this.render();
  }
  render() {
    this.innerHTML = `
      <div class="conversation">
        ${renderConversation(this._messages || [])}
      </div>
    `;
  }
}
customElements.define('chat-view', ChatView);
Key: Logic lives outside component. Component just calls pure functions.
7. Complete Functional Architecture
// State container (like Redux)
const createStore = (initialState) => {
  let state = initialState;
  let listeners = [];
  return {
    getState: () => state,
    setState: (newState) => {
      state = newState;
      listeners.forEach(fn => fn(state));
    },
    subscribe: (fn) => {
      listeners.push(fn);
      return () => listeners = listeners.filter(l => l !== fn);
    }
  };
};
// Initialize
const store = createStore({ messages: [] });
// Pure state updater
const handleInput = (state, input) => {
  const processed = processInput(input);
  const pattern = findMatchingPattern(patterns, processed);
  const response = generateResponse(pattern);
  return createConversationTurn(state.messages, input, response);
};
// Wire it up
const chatView = document.querySelector('chat-view');
const inputEl = document.querySelector('input');
store.subscribe((state) => {
  chatView.messages = state.messages;
});
inputEl.addEventListener('keydown', (e) => {
  if (e.key === 'Enter') {
    const newState = handleInput(store.getState(), e.target.value);
    store.setState(newState);
    e.target.value = '';
  }
});
8. Testing FP Code
Pure functions are trivial to test.
// No mocks, no setup, no teardown
describe('processInput', () => {
  it('normalizes and tokenizes', () => {
    expect(processInput('  Hello WORLD  '))
      .toEqual(['hello', 'world']);
  });
});
describe('addMessage', () => {
  it('returns new array', () => {
    const original = [{ text: 'hi' }];
    const updated = addMessage(original, 'bye');
    expect(updated).not.toBe(original); // different reference
    expect(updated.length).toBe(2);
    expect(original.length).toBe(1); // unchanged
  });
});
Key Takeaways
- Separate data from behavior - Objects carry both, functions just transform
- Make state changes explicit - Immutability makes bugs obvious
- Compose small pieces - Build complexity gradually
- Isolate side effects - Keep core logic pure, effects at edges
- Functions as building blocks - Pass them around like any value
React leverages these ideas quite a bit, but as we see the ability to do this is not a function of a framework. Be careful with FP though is that while it offers great benefits in theory, in practice complexity creeps in and side effects still exist. Like any programming paradigm it is a question of when to apply and how rigidly as opposed to suggesting there is a one perfect way to solve a problem.