Last active
September 16, 2021 04:10
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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