Skip to content

Instantly share code, notes, and snippets.

@gemmadlou
Last active July 14, 2017 21:06
Show Gist options
  • Select an option

  • Save gemmadlou/8567b4889aec09eb793bbc50264d35d6 to your computer and use it in GitHub Desktop.

Select an option

Save gemmadlou/8567b4889aec09eb793bbc50264d35d6 to your computer and use it in GitHub Desktop.
Working Towards Functional Programming
console.clear();
let model = {
data: {
state: 'waiting'
},
set(type, value) {
this.data[type] = value;
for (var i in this.listeners[type]) {
this.listeners[type][i](this.data);
}
},
listeners: {},
observe(type, func) {
if (typeof this.listeners[type] === 'undefined') {
this.listeners[type] = [];
}
if (typeof this.listeners[type]) {
this.listeners[type].push(func);
}
}
}
model.observe('name', (state) => {
console.log('Observed name change', state);
});
model.observe('age', (state) => {
console.log('Observed age change', state);
})
model.observe('state', (state) => {
console.log('Observed state change', state);
})
let changeName = (name) => {
if (model.data.state === 'processing') {
return;
}
model.set('name', name);
model.set('state', 'processing');
setTimeout(() => {
model.set('state', 'waiting');
}, 3000)
}
let changeAge = (age) => {
model.set('age', age)
}
changeName('Gemma')
changeName('Francine')
changeAge(29)
console.clear()
let changeName = (name) => {
if (state.state === 'processing') {
throw new Error('Can not change name during processing')
}
state = Object.assign({}, state);
state.name = name;
return { name: name };
}
let changeAge = (age) => {
state = Object.assign({}, state);
state.age = age;
return state;
}
let startProcess = () => {
state = Object.assign({}, state);
state.state = 'processing';
return state;
}
let stopProcess = () => {
state = Object.assign({}, state);
state.state = 'waiting';
return state;
}
let state = {};
let event = {
nameChanged() {
state = stopProcess(state);
console.log('Name changed', state)
}
}
let handle = {
changeName(name) {
state = startProcess(state);
setTimeout(function() {
state = changeName(state, name);
event.nameChanged();
}, 5000)
}
}
//handle.changeName('Gemma')
handle.changeName('Gemma B')
console.log(state)
window.handle = handle;

Some notes

It seems the best way to look at functional domain modelling, and reaching states is to assume a state, is a single point in time. It is a snapshot of what the state would like at that blip of a moment.

Therefore, I can comfortably manage state changes in a purely functional way, and manage invariants in a purely functional way.

newState = f(state)
a = f(b)

let newState = (state) => {
  // Do some checking
  state.something = 'has changed';
  return state;
}

Example. Both an annoying and satisfying challenge. Modelling a slider.

The task is simple, almost like a counter, a slider goes next or previous, up or down the counter until it reaches the end or the beginning, and then it has to go the next appropriate slide.

The pain point is that there is asynchronicity. That being, a slider may not reach the next slide instantly. It may take several seconds.

let state = {
  currentSlide: 1,
  totalSlides: 5
};
console.clear()
let slider = {
currentSlide: 1,
totalSlides: 5
};
let events = [slider];
let lastEvent = (events) => {
return events[parseInt(events.length - 1)];
}
let getNextSlide = (state) => {
let newstate = Object.assign({}, state);
let nextSlideNumber = newstate.currentSlide === state.totalSlides ? 1 : newstate.currentSlide + 1;
newstate.currentSlide = nextSlideNumber;
return newstate;
}
let getPreviousSlide = (state) => {
let newstate = Object.assign({}, state);
let prevSlideNumber = newstate.currentSlide === 1 ? newstate.totalSlides : newstate.currentSlide - 1;
newstate.currentSlide = prevSlideNumber;
return newstate;
}
events.push(getNextSlide(lastEvent(events)))
events.push(getNextSlide(lastEvent(events)))
events.push(getNextSlide(lastEvent(events)))
events.push(getNextSlide(lastEvent(events)))
events.push(getNextSlide(lastEvent(events)))
events.push(getNextSlide(lastEvent(events)))
events.push(getPreviousSlide(lastEvent(events)))
events.push(getPreviousSlide(lastEvent(events)))
events.push(getPreviousSlide(lastEvent(events)))
console.log(events)

In the example above we also get immutability.

It's done so that every state change, is a snapshot in time. Every event that happens, in the example above though is almost instant. But the conundrum is that - this is a nice example - where everything is being processed at the one time, like on a server, where it receives events through its API.

console.clear()
let bus = {
messages: {},
on(message, callback) {
if (!this.messages[message]) {
this.messages[message] = [];
}
if (typeof callback === 'function') {
this.messages[message].push(callback);
}
},
emit(message, data) {
if (this.messages[message]) {
this.messages[message].forEach((func) => {
func(data);
});
}
}
}
let slider = {
event: 'init',
currentSlide: 1,
totalSlides: 5,
transitioning: false,
upcomingSlide: null
}
class Events {
constructor(initstate) {
this.data = [initstate];
}
add(event) {
this.data.push(event);
}
}
let events = new Events(slider);
let lastEvent = (events) => {
return events.data[parseInt(events.data.length - 1)];
}
let apply = (events, event) => {
if (!event || (event && event.event === undefined)) {
return;
}
events.add(event)
bus.emit(event.event, event);
return events;
}
let gotNextSlide = (state) => {
let newstate = Object.assign({}, state);
if (!newstate.transitioning) {
return;
}
newstate.event = 'gotNextSlide';
newstate.currentSlide = newstate.upcomingSlide;
newstate.transitioning = false;
newstate.upcomingSlide = null;
return newstate;
}
let gotPreviousSlide = (state) => {
let newstate = Object.assign({}, state);
newstate.event = 'getPreviousSlide';
let prevSlideNumber = newstate.currentSlide === 1 ? newstate.totalSlides : newstate.currentSlide - 1;
newstate.currentSlide = prevSlideNumber;
return newstate;
}
let nextSlideRequested = (state) => {
let newstate = Object.assign({}, state);
newstate.event = 'nextSlideRequested';
let currentSlide = newstate.transitioning ? newstate.upcomingSlide : newstate.currentSlide;
let nextSlideNumber = currentSlide === state.totalSlides ? 1 : currentSlide + 1;
newstate.upcomingSlide = nextSlideNumber;
newstate.transitioning = true;
return newstate;
}
bus.on('nextSlideRequested', function(data) {
console.log('Slide requested', data)
setTimeout(() => {
apply(events, gotNextSlide(lastEvent(events)));
}, 1200);
})
apply(events, nextSlideRequested(lastEvent(events)))
apply(events, nextSlideRequested(lastEvent(events)))
apply(events, nextSlideRequested(lastEvent(events)))
apply(events, nextSlideRequested(lastEvent(events)))
console.log(events.data)

A step back to simplicity.

Below again - is a purely functional domain.

console.clear();
class Account {
constructor(name) {
this.name = name;
this.balance = 0;
}
balance() {
return this.balance;
}
}
let debit = (Account, deduction) => {
let account = Object.assign({}, Account);
account.balance = account.balance - deduction;
return account;
}
let credit = (Account, addition) => {
let account = Object.assign({}, Account);
account.balance = account.balance + addition;
return account;
}
let doubleCredit = (Account, amount) => {
let account = Object.assign({}, Account);
return credit(credit(Account, amount), amount);
}
let account = new Account('Gemma');
account = credit(account, 100);
account = debit(account, 50);
account = credit(account, 14.99)
account = doubleCredit(account, 200)
console.log(account)

Something that I actually like:

  • The bus simple. Events are emitted from the model.
  • The view can listen to events.
  • The UI calls command handlers - and shoves in a command.
  • The command handler
console.clear()
let bus = {
messages: {},
on(message, callback) {
if (!this.messages[message]) {
this.messages[message] = [];
}
if (typeof callback === 'function') {
this.messages[message].push(callback);
}
},
emit(message, data) {
if (this.messages[message]) {
this.messages[message].forEach((func) => {
func(data);
});
}
}
}
// Model
let state = {
name: 'Wallace'
}
// Model Behaviours
changeName = (UIstate, name) => {
let state = Object.assign({}, UIstate);
state.name = name;
return state;
}
/*
* The latency is the timeout represents maybe something that
* happens in the database - then it clears for a response.
*
* In the UI - this is clearly not necessary.
*
* And thus we can immediately update the UI
*
* Especially if we have an offline first approach.
*/
// Domain service
changeNameHandler = (command) => {
// Validate command
try {
state = command;
} catch (e) {
bus.emit('nameChangeFailed')
}
setTimeout(() => {
bus.emit('nameChanged');
}, 800)
}
// View Responds to events - not to observables in the state tree.
// That way, the view, if wired correctly, will respond correctly
// to the changes caused by actual commands
bus.on('nameChanged', () => {
let el = document.getElementById('view');
el.innerHTML = state.name
});
// UI Event Handlers
document.querySelector('[name="name"]').addEventListener('keyup', () => {
let name = document.querySelector('[name="name"]').value;
changeNameHandler(changeName(state, name))
})
<textarea name="name"></textarea>
<div id="view"></div>
// Now going real OTT on the view
console.clear()
let bus = {
messages: {},
on(message, callback) {
if (!this.messages[message]) {
this.messages[message] = [];
}
if (typeof callback === 'function') {
this.messages[message].push(callback);
}
},
emit(message, data) {
if (this.messages[message]) {
this.messages[message].forEach((func) => {
func(data);
});
}
}
}
// Model
let state = {
name: 'Wallace'
}
// Model Behaviours
changeName = (UIstate, name) => {
let state = Object.assign({}, UIstate);
state.name = name;
return state;
}
/*
* The latency is the timeout represents maybe something that
* happens in the database - then it clears for a response.
*
* In the UI - this is clearly not necessary.
*
* And thus we can immediately update the UI
*
* Especially if we have an offline first approach.
*/
// Domain service
changeNameHandler = (command) => {
// Validate command
try {
state = command;
} catch (e) {
bus.emit('nameChangeFailed')
}
setTimeout(() => {
bus.emit('nameChanged', state);
}, 800)
}
// View Responds to events - not to observables in the state tree.
// That way, the view, if wired correctly, will respond correctly
// to the changes caused by actual commands
let updateView = (state) => {
document.getElementById('view').innerHTML = state.name;
}
let updateNameView = (state) => {
document.querySelector('[name="name"]').value = state.name
}
bus.on('nameChanged', updateView);
//Init
updateNameView(state);
updateView(state)
// UI Event Handlers
document.querySelector('[name="name"]').addEventListener('keyup', () => {
let name = document.querySelector('[name="name"]').value;
changeNameHandler(changeName(state, name))
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment