Skip to content

Instantly share code, notes, and snippets.

@abiodun0
Last active September 16, 2021 04:10
Show Gist options
  • Save abiodun0/32b798c3841f95b29a811e2f0c1f4603 to your computer and use it in GitHub Desktop.
Save abiodun0/32b798c3841f95b29a811e2f0c1f4603 to your computer and use it in GitHub Desktop.
React and reader monad from first principles https://codesandbox.io/s/react-composition-rjmzd
import ReactDOM from 'react-dom'
import Reac, { createElement } from 'react'
import { compose, map, ap, pipe } from 'ramda'
import IntlMessageFormat from 'intl-messageformat'
const copyrightNotice = View(({ author, year }) => <p>© {author} {year}</p>)
const Monad = {
do: gen => {
let g = gen() // Will need to re-bind generator when done.
const step = value => {
const result = g.next(value)
if (result.done) {
g = gen()
return result.value
} else {
return result.value.chain(step)
}
}
return step()
}
const Reader = computation => ({
map: f => Reader(ctx => f(computation(ctx))),
contramap: f => Reader(ctx => computation(f(ctx))),
chain: f => {
return Reader(ctx => {
// Get the result from original computation
const a = computation(ctx)
// Now get the result from the computation
// inside the Reader `f(a)`.
return f(a).runReader(ctx)
})
},
ap: other => Reader(ctx => computation(ctx)(other.runReader(ctx))),
runReader: ctx => computation(ctx)
})
Reader.of = x => Reader(() => x)
Reader.ask = () => Reader(x => x)
Reader.asks = fn => Reader(fn)
const asArray = x => (Array.isArray(x) ? x : Array.of(x))
const View = pipe(
x => compose(asArray, x),
computation => ({
computation, // This is used during chain, concat, etc.
// Not meant to be used publicly.
fold: props => {
const result = computation(props).filter(x => x !== null)
// If we only have one element to render, don't wrap it with div.
// This preserves the view's root element if it only has one root.
// e.g. View.of(<div/>).fold() is just <div/>
if (result.length === 1) {
return result[0]
// If the computation results in multiple items, then wrap it in a
// parent div. This is needed because React cannot render an array.
// See: https://github.com/facebook/react/issues/2127
} else {
return createElement('div', { children: result })
}
},
map: f => View(x => computation(x).map(f)),
ap: other => (
View(props => (ap(computation(props), other.computation(props))))
),
contramap: g => View(x => computation(g(x))),
concat: other =>
View(props => computation(props).concat(other.computation(props))),
chain: g => View(x => computation(x).concat(y => g(y).computation(x))),
promap: (g, f) => View(x => computation(g(x)).map(f))
})
)
View.of = x => View(() => x)
View.empty = () => View.of(null)
const footer = Reader(({ author, year }) =>
copyrightNotice.contramap(() => ({ author, year }))
)
const footer2 = footer.map(map(y => <footer>{y}</footer>))
const pageTitle = View(({ title }) => <h1>{title}</h1>)
const header = Reader(({ title }) =>
pageTitle.contramap(() => ({ title }))
).map(map(x => <header>{x}</header>))
const combined =
// Map over the view inside header
header.map(headerView =>
// Then map over the view inside footer
footer2.map(footerView =>
// Concat both views together
headerView.concat(footerView)
)
)
const ctx = {
year: 2017,
author: 'Bob McBob',
title: 'Awesome App'
}
const a = Reader.ask()
.chain(x => Reader.of(x + 1))
.chain(y => Reader.of(y * 2))
.chain(z => Reader.of(`Got ${z}!`))
// console.log(a.runReader(1))
// console.log(a.runReader(0))
// console.log(a.runReader(-1))
const b = Monad.do(function*() {
const ctx = yield Reader.ask()
const x = yield Reader.of(ctx + 1)
const y = x * 2
const z = `Got ${y}!`
return Reader.of(z)
})
// console.log(b.runReader(1))
// console.log(b.runReader(0))
// console.log(b.runReader(-1))
const makeColor = ({ color }) => view =>
view.map(element => <div style={{ color }}>{element}</div>)
const withColor = Reader(makeColor)
const combined2 =
// We need to use chain here since the result of the function
// is another Reader.
header.chain(headerView =>
footer2.map(footerView => headerView.concat(footerView))
)
const x = withColor.ap(footer2)
const messages = {
en: { hello: 'Hello {name}!' },
es: { hello: 'Hola {name}!' },
fr: { hello: 'Bonjour {name}!' },
zh: { hello: '你好 {name}!' }
}
const formatMessage = locale => id => values => {
return new IntlMessageFormat(messages[locale][id], locale).format(values)
}
// console.log(formatMessage('fr')('hello')({ name: 'Alice' }))
const formatWithLocale = Monad.do(function*() {
const { locale } = yield Reader.ask()
return Reader.of(formatMessage(locale))
})
const formatHello = formatWithLocale.ap(Reader.of('hello'))
console.log(formatHello.runReader({ locale: 'fr' })({ name: 'Alice' }))
const clap = x => <span>👏 {x} 👏</span>
const uppercase = x => <span style={{ textTransform: 'uppercase' }}>{x}</span>
const emphasize = x => <span style={{ fontStyle: 'italic' }}>{x}</span>
const message = Reader.of(View(({ message }) => <p>{message}</p>))
const app = Monad.do(function*() {
// Let's colorize our header shall we?
const headerView = yield withColor.ap(header)
// This will translate our message!
const sayHello = yield formatHello
// The message needs to be mapped using the context-aware `sayHello` function.
const messageView = yield message.map(x =>
// The `name` prop will be provided when we `fold` the view.
x.contramap(({ name }) => ({
message: compose(
clap,
emphasize,
uppercase,
sayHello
)({ name })
})
))
const footerView = yield footer2
// Combine everything together!
return Reader.of(headerView.concat(messageView).concat(footerView))
})
// Hmm, let's style the element as well!
const styledApp = app.map(
map(x => (
<div style={{ fontFamily: 'Helvetica', textAlign: 'center' }}>{x}</div>
))
)
export default element => {
ReactDOM.render(
styledApp
.runReader({
color: 'green',
locale: 'zh',
title: 'Awesome App',
author: 'Bob McBob',
year: 2017
})
.fold({ name: 'Alice' }),
element
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment