├── blog └── index.js ├── free └── 0.js ├── function-modelling └── 0.js ├── lenses └── 0.js ├── lib ├── db.js ├── free.js ├── types.js └── validations.js ├── monoids ├── 0.js └── 1.js ├── package.json ├── todont ├── index.css ├── index.html ├── index.js ├── package.json ├── ui.js ├── webpack.config.js └── yarn.lock └── transformers ├── 0.js ├── 1.js └── 2.js /blog/index.js: -------------------------------------------------------------------------------- 1 | const {save, all} = require('../lib/db') 2 | const {Task, Id} = require('../lib/types') 3 | const {Free, liftF} = require('../lib/free') 4 | const {Pure} = Free 5 | const {last} = require('ramda') 6 | const {taggedSum} = require('daggy') 7 | 8 | const Console = taggedSum('Console', {Question: ['q'], Print: ['s']}) 9 | const Db = taggedSum('Db', {Save: ['table', 'record'], All: ['table', 'query']}) 10 | 11 | const AuthorTable = "Authors" 12 | const PostTable = "Post" 13 | 14 | const Author = name => ({name}) 15 | const Post = (title, body) => ({title, body}) 16 | 17 | // Thanks flavio! 18 | const readline = require('readline').createInterface({input: process.stdin, output: process.stdout}) 19 | 20 | const writeOutput = s => Task((_rej, res) => res(console.log(s))) 21 | const getInput = (q) => 22 | Task((rej, res) => readline.question(q, i => res(i.trim()))) 23 | 24 | const formatPost = post => `${post.title}:\n${post.body}` 25 | 26 | const print = s => liftF(Console.Print(s)) 27 | const question = s => liftF(Console.Question(s)) 28 | 29 | const dbAll = (table, query) => liftF(Db.All(table, query)) 30 | const dbSave = (table, record) => liftF(Db.Save(table, record)) 31 | 32 | const questions = { 33 | whereTo: 'Where do you want to go today? (createAuthor, write, latest, all, end)', 34 | title: 'Title? ', 35 | body: 'Body? ', 36 | name: 'Name? ' 37 | } 38 | 39 | const menu = () => 40 | question(questions.whereTo) 41 | .map(route => router[route]()) 42 | 43 | const latest = () => 44 | dbAll(PostTable) 45 | .map(posts => last(posts)) 46 | .map(formatPost) 47 | .chain(print) 48 | .map(() => menu()) 49 | 50 | const write = () => 51 | question(questions.title) 52 | .chain(title => 53 | question(questions.body) 54 | .map(body => Post(title, body)) 55 | ) 56 | .chain(post => dbSave(PostTable, post)) 57 | .map(() => latest()) 58 | 59 | const createAuthor = () => 60 | question(questions.name) 61 | .map(name => Author(name)) 62 | .chain(author => dbSave(AuthorTable, author)) 63 | .map(() => menu()) 64 | 65 | const start = () => 66 | dbAll(AuthorTable) 67 | .map(authors => authors.length ? menu() : createAuthor()) 68 | 69 | const end = () => Pure(Console.Print('fin')) 70 | 71 | const router = {menu, createAuthor, write, latest, end} 72 | 73 | const answers = { 74 | [questions.whereTo]: 'end', 75 | [questions.title]: 'A title', 76 | [questions.body]: 'A body', 77 | [questions.name]: 'Some name' 78 | } 79 | 80 | const dbToTask = x => 81 | x.cata({ Save: save, All: all }) 82 | 83 | const consoleToTask = x => 84 | x.cata({ Question: getInput, Print: writeOutput }) 85 | 86 | const consoleToId = x => 87 | x.cata({ 88 | Question: (q) => Id.of(answers[q]), 89 | Print: (s) => Id.of(`writing the string ${s}`) 90 | }) 91 | 92 | const dbToId = x => 93 | x.cata({ 94 | Save: (table, r) => Id.of(`saving to ${r} ${table}`), 95 | All: (table, q) => Id.of([]) 96 | }) 97 | 98 | const interpretTest = free => 99 | Task.of(free.foldMap(x => x.table ? dbToId(x) : consoleToId(x), Id.of).extract()) 100 | 101 | const interpretReal = free => 102 | free.foldMap(x => x.table ? dbToTask(x) : consoleToTask(x), Task.of) 103 | 104 | const runApp = (free, interpreter, log=[]) => 105 | free.cata({ 106 | Pure: x => log.concat([x]), 107 | Impure: x => interpreter(free).fork(console.error, g => runApp(g, interpreter, log.concat([x]))) 108 | }) 109 | 110 | const result = runApp(start(), interpretTest) 111 | console.log(result) 112 | -------------------------------------------------------------------------------- /free/0.js: -------------------------------------------------------------------------------- 1 | const {liftF} = require('../lib/free') 2 | const {Id} = require('../lib/types') 3 | const {taggedSum} = require('daggy') 4 | 5 | const Http = taggedSum('Http', {Get: ['url'], Post: ['url', 'body']}) 6 | 7 | const httpGet = (url) => liftF(Http.Get(url)) 8 | const httpPost = (url, body) => liftF(Http.Post(url, body)) 9 | 10 | const app = () => 11 | httpGet('/home') 12 | .chain(contents => httpPost('/analytics', contents)) 13 | 14 | const interpret = x => 15 | x.cata({ 16 | Get: url => Id.of(`contents for ${url}`), 17 | Post: (url, body) => Id.of(`posted ${body} to ${url}`) 18 | }) 19 | 20 | const res = app().foldMap(interpret, Id.of) 21 | console.log(res.extract()) -------------------------------------------------------------------------------- /function-modelling/0.js: -------------------------------------------------------------------------------- 1 | const {Either} = require("../lib/types") 2 | const {List} = require("immutable-ext") 3 | 4 | const toUpper = x => x.toUpperCase() 5 | const exclaim = x => x.concat('!') 6 | 7 | const Fn = run => 8 | ({ 9 | run, 10 | chain: f => Fn(x => f(run(x)).run(x)), 11 | map: f => Fn(x => f(run(x))), 12 | concat: other => 13 | Fn(x => run(x).concat(other.run(x))) 14 | }) 15 | 16 | Fn.ask = Fn(x => x) 17 | Fn.of = x => Fn(() => x) 18 | 19 | const Endo = run => 20 | ({ 21 | run, 22 | concat: other => 23 | Endo(x => other.run(run(x))) 24 | }) 25 | Endo.empty = () => Endo(x => x) 26 | 27 | const res = List([toUpper, exclaim]) 28 | .foldMap(Endo, Endo.empty()) 29 | .run('hello') 30 | 31 | 32 | // (acc, a) -> acc 33 | // (a, acc) -> acc 34 | // a -> (acc -> acc) 35 | // a -> Endo(acc -> acc) 36 | 37 | // Fn(a -> Endo(acc -> acc)) 38 | const Reducer = run => 39 | ({ 40 | run, 41 | contramap: f => 42 | Reducer((acc, x) => run(acc, f(x))), 43 | concat: other => 44 | Reducer((acc, x) => other.run(run(acc, x), x)) 45 | }) 46 | 47 | const checkCreds = (email, pass) => 48 | email === 'admin' && pass === 123 49 | 50 | const login = payload => state => 51 | payload.email 52 | ? Object.assign({}, state, {loggedIn: checkCreds(payload.email, payload.pass)}) 53 | : state 54 | 55 | const setPrefs = payload => state => 56 | payload.prefs 57 | ? Object.assign({}, state, {prefs: payload.prefs}) 58 | : state 59 | 60 | const reducer = Fn(login).map(Endo).concat(Fn(setPrefs).map(Endo)) 61 | 62 | const state = {loggedIn: false, prefs: {}} 63 | const payload = {email: 'dmin', pass: 123, prefs: {bgColor: '#000'}} 64 | console.log(reducer.run(payload).run(state)) -------------------------------------------------------------------------------- /lenses/0.js: -------------------------------------------------------------------------------- 1 | const {toUpper, view, over, lensProp, compose} = require('ramda') 2 | 3 | const L = { 4 | _0: lensProp(0), 5 | name: lensProp('name'), 6 | street: lensProp('street'), 7 | address: lensProp('address') 8 | } 9 | 10 | const users = [{address: {street: {name: 'Maple'}}}] 11 | const addrStreetName = compose(L._0, L.address, L.street, L.name) 12 | const res = over(addrStreetName, toUpper, users) 13 | console.log(res[0]) 14 | 15 | -------------------------------------------------------------------------------- /lib/db.js: -------------------------------------------------------------------------------- 1 | // import { stat, writeFile } from "fs" 2 | const {Task, Either} = require("./types") 3 | // import path from "path" 4 | const I = require("immutable-ext") 5 | 6 | // strategy...reader... 7 | 8 | // // File (transformers?) 9 | // const rootPath = x => path.resolve('db', x) 10 | 11 | // const findOrCreateFile = (path) => 12 | // stat(path) // Task(dir) 13 | // .fold(() => touch(path), () => Task.of(path)) 14 | 15 | // const loadTable = tableName => 16 | // findOrCreateFile(rootPath(tableName)) 17 | // .chain(readFile) 18 | // .chain(parse) 19 | // .fold(() => Task.of([])) 20 | 21 | // const store = (tableName, table) => 22 | // stringify(table) 23 | // .chain(table => writeFile(rootPath(tableName), table)) 24 | // .map(() => table) 25 | 26 | // const genId = table => 27 | // last(table).fold(() => 0, x => x.id + 1) 28 | 29 | // const addRecord = (table, record) => 30 | // table.concat(record) 31 | 32 | // const findAll = (table) => 33 | // Task.of(table.values()) 34 | 35 | // const find = (table, query) => 36 | // Task.of(table.filter((v, k) => query[k] === v)) 37 | 38 | // Array 39 | const STORE = new Map() 40 | 41 | const loadTable = tableName => 42 | Task.of(STORE.get(tableName) || I.List()) 43 | 44 | const store = (tableName, table) => 45 | Task.of(STORE.set(tableName, table)) 46 | 47 | const genId = table => 48 | table.count() 49 | 50 | const addRecord = (table, record) => 51 | Task.of(table.push(record)) 52 | 53 | const getAll = (table) => 54 | Task.of(table.toJS()) 55 | 56 | const queryAll = (table, query) => 57 | Task.of(table.filter((v, k) => query[k] === v)) 58 | 59 | 60 | // db 61 | const addId = (obj, table) => 62 | Object.assign({id: genId(table)}, obj) 63 | 64 | const save = (tableName, obj) => 65 | loadTable(tableName) 66 | .chain(table => addRecord(table, addId(obj, table))) 67 | .chain(newTable => store(tableName, newTable)) 68 | 69 | const all = tableName => 70 | loadTable(tableName) 71 | .chain(table => getAll(table)) 72 | 73 | const find = (tableName, query) => 74 | loadTable(tableName) 75 | .chain(table => queryAll(table, query)) 76 | 77 | module.exports = {save, find, all, STORE} -------------------------------------------------------------------------------- /lib/free.js: -------------------------------------------------------------------------------- 1 | const daggy = require('daggy') 2 | 3 | const Free = daggy.taggedSum('Free', {Impure: ['x', 'f'], Pure: ['x']}) 4 | 5 | Free.of = Free.Pure 6 | 7 | const kleisli_comp = (f, g) => x => f(x).chain(g) 8 | 9 | Free.prototype.fold = function() { 10 | return this.x.fold.apply(this.x, arguments) 11 | } 12 | 13 | 14 | Free.prototype.map = function(f) { 15 | return this.cata({ 16 | Impure: (x, g) => Free.Impure(x, y => g(y).map(f)), 17 | Pure: x => Free.Pure(f(x)) 18 | }) 19 | } 20 | 21 | Free.prototype.ap = function(a) { 22 | return this.cata({ 23 | Impure: (x, g) => Free.Impure(x, y => g(y).ap(a)), 24 | Pure: f => a.map(f) 25 | }) 26 | } 27 | 28 | Free.prototype.chain = function(f) { 29 | return this.cata({ 30 | Impure: (x, g) => Free.Impure(x, kleisli_comp(g, f)), 31 | Pure: x => f(x) 32 | }) 33 | } 34 | 35 | const liftF = command => Free.Impure(command, Free.Pure) 36 | 37 | Free.prototype.foldMap = function(interpreter, of) { 38 | return this.cata({ 39 | Pure: a => of(a), 40 | Impure: (intruction_of_arg, next) => 41 | interpreter(intruction_of_arg).chain(result => 42 | next(result).foldMap(interpreter, of)) 43 | }) 44 | } 45 | 46 | module.exports = { liftF, Free } 47 | -------------------------------------------------------------------------------- /lib/types.js: -------------------------------------------------------------------------------- 1 | const Reducer = run => 2 | ({ 3 | run, 4 | concat: other => Reducer((acc, x) => other.run(run(acc, x), x)), 5 | contramap: f => Reducer((acc, x) => run(acc, f(x))), 6 | map: f => Reducer((acc, x) => f(run(acc, x))) 7 | }) 8 | 9 | const Id = x => 10 | ({ 11 | map:f => Id(f(x)), 12 | chain:f => f(x), 13 | extract: () => x, 14 | concat: (o) => Id(x.concat(o.extract())) 15 | }) 16 | Id.of = x => Id(x) 17 | 18 | const IdT = M => { 19 | const Id = mx => 20 | ({ 21 | map:f => Id(mx.map(f)), 22 | chain:f => Id(mx.chain(x => f(x).extract())), 23 | extract: () => mx 24 | }) 25 | Id.of = x => Id(M.of(x)) 26 | Id.lift = mx => Id(mx) 27 | return Id 28 | } 29 | 30 | const IO = run => 31 | ({ 32 | run, 33 | map: f => IO(() => f(run())), 34 | chain: f => IO(() => f(run()).run()), 35 | concat: other => IO(() => run().concat(other.run())) 36 | }) 37 | IO.of = x => IO(() => x) 38 | 39 | const Fn = g => 40 | ({ 41 | map: f => 42 | Fn(x => f(g(x))), 43 | chain: f => 44 | Fn(x => f(g(x)).run(x)), 45 | concat: other => 46 | Fn(x => g(x).concat(other.run(x))), 47 | run: g 48 | }) 49 | Fn.ask = Fn(x => x) 50 | Fn.of = x => Fn(() => x) 51 | 52 | const FnT = M => { 53 | const Fn = g => 54 | ({ 55 | map: f => 56 | Fn(x => g(x).map(f)), 57 | chain: f => 58 | Fn(x => g(x).chain(y => f(y).run(x))), 59 | concat: other => 60 | Fn(x => g(x).concat(other.run(x))), 61 | run: g 62 | }) 63 | Fn.ask = Fn(x => M.of(x)) 64 | Fn.of = x => Fn(() => M.of(x)) 65 | Fn.lift = x => Fn(() => x) 66 | return Fn 67 | } 68 | 69 | const Either = (() => { 70 | const Right = x => 71 | ({ 72 | isLeft: false, 73 | chain: f => f(x), 74 | ap: other => other.map(x), 75 | alt: other => Right(x), 76 | extend: f => f(Right(x)), 77 | concat: other => 78 | other.fold(x => other, 79 | y => Right(x.concat(y))), 80 | traverse: (of, f) => f(x).map(Right), 81 | map: f => Right(f(x)), 82 | fold: (_, g) => g(x), 83 | toString: () => `Right(${x})` 84 | }) 85 | 86 | const Left = x => 87 | ({ 88 | isLeft: true, 89 | chain: _ => Left(x), 90 | ap: _ => Left(x), 91 | extend: _ => Left(x), 92 | alt: other => other, 93 | concat: _ => Left(x), 94 | traverse: (of, _) => of(Left(x)), 95 | map: _ => Left(x), 96 | fold: (f, _) => f(x), 97 | toString: () => `Left(${x})` 98 | }) 99 | 100 | const of = Right; 101 | const tryCatch = f => { 102 | try { 103 | return Right(f()) 104 | } catch(e) { 105 | return Left(e) 106 | } 107 | } 108 | 109 | const fromNullable = x => 110 | x != null ? Right(x) : Left(x) 111 | 112 | return {Right, Left, of, tryCatch, fromNullable } 113 | })() 114 | 115 | const EitherT = M => { 116 | const Right = mx => 117 | ({ 118 | isLeft: false, 119 | extract: () => mx, 120 | chain: f => Right(mx.chain(x => f(x).extract())), 121 | map: f => Right(mx.map(f)), 122 | fold: (_, g) => g(mx) 123 | }) 124 | 125 | const Left = mx => 126 | ({ 127 | isLeft: true, 128 | extract: () => mx, 129 | chain: _ => Left(mx), 130 | map: _ => Left(mx), 131 | fold: (h, _) => h(mx) 132 | }) 133 | 134 | const of = x => Right(M.of(x)) 135 | const tryCatch = f => { 136 | try { 137 | return Right(M.of(f())) 138 | } catch(e) { 139 | return Left(e) 140 | } 141 | } 142 | 143 | const lift = Right 144 | 145 | return {of, tryCatch, lift, Right, Left } 146 | } 147 | 148 | const Task = fork => 149 | ({ 150 | fork, 151 | ap: other => 152 | Task((rej, res) => fork(rej, f => other.fork(rej, x => res(f(x))))), 153 | map: f => 154 | Task((rej, res) => fork(rej, x => res(f(x)))), 155 | chain: f => 156 | Task((rej, res) => fork(rej, x => f(x).fork(rej, res))), 157 | concat: other => 158 | Task((rej, res) => fork(rej, x => other.fork(rej, y => res(x.concat(y))))), 159 | fold: (f, g) => 160 | Task((rej, res) => fork(x => f(x).fork(rej, res), x => g(x).fork(rej, res))) 161 | }) 162 | Task.of = x => Task((rej, res) => res(x)) 163 | Task.rejected = x => Task((rej, res) => rej(x)) 164 | Task.fromPromised = fn => (...args) => Task((rej, res) => fn(...args).then(res).catch(rej)) 165 | 166 | const TaskT = M => { 167 | const Task = fork => 168 | ({ 169 | fork, 170 | map: f => 171 | Task((rej, res) => fork(rej, mx => res(mx.map(f)))), 172 | chain: f => 173 | Task((rej, res) => 174 | fork(rej, mx => 175 | mx.chain(x => f(x).fork(rej, res)))) 176 | }) 177 | Task.lift = x => Task((rej, res) => res(x)) 178 | Task.of = x => Task((rej, res) => res(M.of(x))) 179 | Task.rejected = x => Task((rej, res) => rej(x)) 180 | 181 | return Task 182 | } 183 | 184 | const State = run => ({ 185 | run, 186 | chain: f => 187 | State(x => { 188 | const [y, s] = run(x); 189 | return f(y).run(s) 190 | }), 191 | map: f => 192 | State(x => { 193 | const [y, s] = run(x); 194 | return [f(y), s]; 195 | }), 196 | concat: other => 197 | State(x => { 198 | const [y, s] = run(x); 199 | const [y1, _s1] = other.run(x); 200 | return [y.concat(y1), s]; 201 | }) 202 | }); 203 | 204 | State.of = x => State(s => [x, s]) 205 | State.get = State(x => [x, x]) 206 | State.modify = f => State(s => [null, f(s)]) 207 | State.put = x => State(s => [null, x]) 208 | 209 | const StateT = M => { 210 | const State = run => ({ 211 | run, 212 | chain: f => 213 | State(x => run(x).chain(([y, s]) => f(y).run(s))), 214 | map: f => 215 | State(x => run(x).map(([y, s]) => [f(y), s])), 216 | concat: other => 217 | State(x => 218 | run(x).chain(([y, s]) => 219 | other.run(x).map(([y1, s1]) => 220 | [y.concat(y1), s] 221 | ) 222 | ) 223 | ) 224 | }); 225 | 226 | State.lift = m => State(s => m.map(x => [x, s])) 227 | State.of = x => State(s => M.of([x, s])) 228 | State.get = State(x => M.of([x, x])) 229 | State.modify = f => State(s => M.of([null, f(s)])) 230 | State.put = x => State(s => M.of([null, x])) 231 | 232 | return State 233 | } 234 | 235 | 236 | module.exports = {Id, IdT, Task, TaskT, State, StateT, Fn, FnT, Either, EitherT, IO, Reducer} -------------------------------------------------------------------------------- /lib/validations.js: -------------------------------------------------------------------------------- 1 | const {List} = require('immutable-ext') 2 | const {Either} = require('./types') 3 | const {Left, Right} = Either 4 | 5 | const Success = x => 6 | ({ 7 | isFail: false, 8 | x, 9 | fold: (f, g) => g(x), 10 | concat: other => 11 | other.isFail ? other : Success(x) 12 | }) 13 | 14 | const Fail = x => 15 | ({ 16 | isFail: true, 17 | fold: (f, g) => f(x), 18 | x, 19 | concat: other => 20 | other.isFail ? Fail(x.concat(other.x)) : Fail(x) 21 | }) 22 | 23 | const Validation = run => 24 | ({ 25 | run, 26 | concat: other => 27 | Validation((key, x) => run(key, x).concat(other.run(key, x))) 28 | }) 29 | 30 | const isEmail = Validation((key, x) => 31 | /@/.test(x) 32 | ? Success(x) 33 | : Fail([`${key} must be an email`]) 34 | ) 35 | 36 | const isPresent = Validation((key, x) => 37 | !!x 38 | ? Success(x) 39 | : Fail([`${key} needs to be present`]) 40 | ) 41 | 42 | const validate = (spec, obj) => 43 | List(Object.keys(spec)).foldMap(key => 44 | spec[key].run(key, obj[key]) 45 | , Success([obj])) 46 | 47 | module.exports = {validate} -------------------------------------------------------------------------------- /monoids/0.js: -------------------------------------------------------------------------------- 1 | const {List} = require('immutable-ext') 2 | // Monoid = Semigroup + Identity 3 | 4 | const Product = x => 5 | ({ 6 | x, 7 | concat: other => 8 | Product(x * other.x) 9 | }) 10 | Product.empty = () => Product(1) 11 | 12 | const Sum = x => 13 | ({ 14 | x, 15 | concat: other => 16 | Sum(x + other.x) 17 | }) 18 | Sum.empty = () => Sum(0) 19 | 20 | const Any = x => 21 | ({ 22 | x, 23 | concat: other => 24 | Any(x || other.x) 25 | }) 26 | 27 | Any.empty = () => Any(false) 28 | 29 | const All = x => 30 | ({ 31 | x, 32 | concat: other => 33 | All(x && other.x) 34 | }) 35 | 36 | All.empty = () => All(true) 37 | 38 | const Intersection = x => 39 | ({ 40 | x, 41 | concat: other => 42 | Intersection(_.intersection(x, other.x)) 43 | }) 44 | 45 | Intersection([1,2,3,4]).concat(Intersection([12,3,4,5])) 46 | 47 | 48 | 49 | const res = List([true, true, false]).foldMap(All, All.empty()) 50 | 51 | console.log(res) -------------------------------------------------------------------------------- /monoids/1.js: -------------------------------------------------------------------------------- 1 | const {Id, Task, Either} = require('../lib/types') 2 | const {Left, Right} = Either 3 | const {List} = require('immutable-ext') 4 | 5 | const Product = x => 6 | ({ 7 | x, 8 | concat: other => 9 | Product(x * other.x) 10 | }) 11 | Product.empty = () => Product(1) 12 | 13 | const Sum = x => 14 | ({ 15 | x, 16 | concat: other => 17 | Sum(x + other.x) 18 | }) 19 | Sum.empty = () => Sum(0) 20 | 21 | const Any = x => 22 | ({ 23 | x, 24 | concat: other => 25 | Any(x || other.x) 26 | }) 27 | 28 | Any.empty = () => Any(false) 29 | 30 | const All = x => 31 | ({ 32 | x, 33 | concat: other => 34 | All(x && other.x) 35 | }) 36 | 37 | All.empty = () => All(true) 38 | 39 | const id = x => x 40 | 41 | const Alternative = ex => 42 | ({ 43 | ex, 44 | concat: other => 45 | Alternative(other.ex.isLeft ? ex : ex.concat(other.ex)) 46 | }) 47 | 48 | "https://codepen.io/drboolean/pen/MpKpee?editors=0010" 49 | 50 | const res = List([Right('a'), Left('b'), Right('c')]) 51 | .foldMap(Alternative, Alternative(Right(''))) 52 | 53 | console.log(res.ex.fold(id, id)) 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "daggy": "^1.4.0", 4 | "immutable": "^4.0.0-rc.12", 5 | "immutable-ext": "^1.1.5", 6 | "lodash": "^4.17.15", 7 | "ramda": "^0.26.1" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /todont/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | button { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | background: none; 12 | font-size: 100%; 13 | vertical-align: baseline; 14 | font-family: inherit; 15 | font-weight: inherit; 16 | color: inherit; 17 | -webkit-appearance: none; 18 | appearance: none; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-font-smoothing: antialiased; 21 | font-smoothing: antialiased; 22 | } 23 | 24 | body { 25 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 26 | line-height: 1.4em; 27 | background: #f5f5f5; 28 | color: #4d4d4d; 29 | min-width: 230px; 30 | max-width: 550px; 31 | margin: 0 auto; 32 | -webkit-font-smoothing: antialiased; 33 | -moz-font-smoothing: antialiased; 34 | font-smoothing: antialiased; 35 | font-weight: 300; 36 | } 37 | 38 | button, 39 | input[type="checkbox"] { 40 | outline: none; 41 | } 42 | 43 | .hidden { 44 | display: none; 45 | } 46 | 47 | .todoapp { 48 | background: #fff; 49 | margin: 130px 0 40px 0; 50 | position: relative; 51 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 52 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 53 | } 54 | 55 | .todoapp input::-webkit-input-placeholder { 56 | font-style: italic; 57 | font-weight: 300; 58 | color: #e6e6e6; 59 | } 60 | 61 | .todoapp input::-moz-placeholder { 62 | font-style: italic; 63 | font-weight: 300; 64 | color: #e6e6e6; 65 | } 66 | 67 | .todoapp input::input-placeholder { 68 | font-style: italic; 69 | font-weight: 300; 70 | color: #e6e6e6; 71 | } 72 | 73 | .todoapp h1 { 74 | position: absolute; 75 | top: -155px; 76 | width: 100%; 77 | font-size: 100px; 78 | font-weight: 100; 79 | text-align: center; 80 | color: rgba(175, 47, 47, 0.15); 81 | -webkit-text-rendering: optimizeLegibility; 82 | -moz-text-rendering: optimizeLegibility; 83 | text-rendering: optimizeLegibility; 84 | } 85 | 86 | .new-todo, 87 | .edit { 88 | position: relative; 89 | margin: 0; 90 | width: 100%; 91 | font-size: 24px; 92 | font-family: inherit; 93 | font-weight: inherit; 94 | line-height: 1.4em; 95 | border: 0; 96 | outline: none; 97 | color: inherit; 98 | padding: 6px; 99 | border: 1px solid #999; 100 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 101 | box-sizing: border-box; 102 | -webkit-font-smoothing: antialiased; 103 | -moz-font-smoothing: antialiased; 104 | font-smoothing: antialiased; 105 | } 106 | 107 | .new-todo { 108 | padding: 16px 16px 16px 60px; 109 | border: none; 110 | background: rgba(0, 0, 0, 0.003); 111 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 112 | } 113 | 114 | .main { 115 | position: relative; 116 | z-index: 2; 117 | border-top: 1px solid #e6e6e6; 118 | } 119 | 120 | label[for='toggle-all'] { 121 | display: none; 122 | } 123 | 124 | .toggle-all { 125 | position: absolute; 126 | top: -55px; 127 | left: -12px; 128 | width: 60px; 129 | height: 34px; 130 | text-align: center; 131 | border: none; /* Mobile Safari */ 132 | } 133 | 134 | .toggle-all:before { 135 | content: '❯'; 136 | font-size: 22px; 137 | color: #e6e6e6; 138 | padding: 10px 27px 10px 27px; 139 | } 140 | 141 | .toggle-all:checked:before { 142 | color: #737373; 143 | } 144 | 145 | .todo-list { 146 | margin: 0; 147 | padding: 0; 148 | list-style: none; 149 | } 150 | 151 | .todo-list li { 152 | position: relative; 153 | font-size: 24px; 154 | border-bottom: 1px solid #ededed; 155 | } 156 | 157 | .todo-list li:last-child { 158 | border-bottom: none; 159 | } 160 | 161 | .todo-list li.editing { 162 | border-bottom: none; 163 | padding: 0; 164 | } 165 | 166 | .todo-list li.editing .edit { 167 | display: block; 168 | width: 506px; 169 | padding: 13px 17px 12px 17px; 170 | margin: 0 0 0 43px; 171 | } 172 | 173 | .todo-list li.editing .view { 174 | display: none; 175 | } 176 | 177 | .todo-list li .toggle { 178 | text-align: center; 179 | width: 40px; 180 | /* auto, since non-WebKit browsers doesn't support input styling */ 181 | height: auto; 182 | position: absolute; 183 | top: 0; 184 | bottom: 0; 185 | margin: auto 0; 186 | border: none; /* Mobile Safari */ 187 | -webkit-appearance: none; 188 | appearance: none; 189 | } 190 | 191 | .todo-list li .toggle:after { 192 | content: url('data:image/svg+xml;utf8,'); 193 | } 194 | 195 | .todo-list li .toggle:checked:after { 196 | content: url('data:image/svg+xml;utf8,'); 197 | } 198 | 199 | .todo-list li label { 200 | white-space: pre-line; 201 | word-break: break-all; 202 | padding: 15px 60px 15px 15px; 203 | margin-left: 45px; 204 | display: block; 205 | line-height: 1.2; 206 | transition: color 0.4s; 207 | } 208 | 209 | .todo-list li.completed label { 210 | color: #d9d9d9; 211 | text-decoration: line-through; 212 | } 213 | 214 | .todo-list li .destroy { 215 | display: none; 216 | position: absolute; 217 | top: 0; 218 | right: 10px; 219 | bottom: 0; 220 | width: 40px; 221 | height: 40px; 222 | margin: auto 0; 223 | font-size: 30px; 224 | color: #cc9a9a; 225 | margin-bottom: 11px; 226 | transition: color 0.2s ease-out; 227 | } 228 | 229 | .todo-list li .destroy:hover { 230 | color: #af5b5e; 231 | } 232 | 233 | .todo-list li .destroy:after { 234 | content: '×'; 235 | } 236 | 237 | .todo-list li:hover .destroy { 238 | display: block; 239 | } 240 | 241 | .todo-list li .edit { 242 | display: none; 243 | } 244 | 245 | .todo-list li.editing:last-child { 246 | margin-bottom: -1px; 247 | } 248 | 249 | .footer { 250 | color: #777; 251 | padding: 10px 15px; 252 | height: 20px; 253 | text-align: center; 254 | border-top: 1px solid #e6e6e6; 255 | } 256 | 257 | .footer:before { 258 | content: ''; 259 | position: absolute; 260 | right: 0; 261 | bottom: 0; 262 | left: 0; 263 | height: 50px; 264 | overflow: hidden; 265 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 266 | 0 8px 0 -3px #f6f6f6, 267 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 268 | 0 16px 0 -6px #f6f6f6, 269 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 270 | } 271 | 272 | .todo-count { 273 | float: left; 274 | text-align: left; 275 | } 276 | 277 | .todo-count strong { 278 | font-weight: 300; 279 | } 280 | 281 | .filters { 282 | margin: 0; 283 | padding: 0; 284 | list-style: none; 285 | position: absolute; 286 | right: 0; 287 | left: 0; 288 | } 289 | 290 | .filters li { 291 | display: inline; 292 | } 293 | 294 | .filters li a { 295 | color: inherit; 296 | margin: 3px; 297 | padding: 3px 7px; 298 | text-decoration: none; 299 | border: 1px solid transparent; 300 | border-radius: 3px; 301 | } 302 | 303 | .filters li a.selected, 304 | .filters li a:hover { 305 | border-color: rgba(175, 47, 47, 0.1); 306 | } 307 | 308 | .filters li a.selected { 309 | border-color: rgba(175, 47, 47, 0.2); 310 | } 311 | 312 | .clear-completed, 313 | html .clear-completed:active { 314 | float: right; 315 | position: relative; 316 | line-height: 20px; 317 | text-decoration: none; 318 | cursor: pointer; 319 | position: relative; 320 | } 321 | 322 | .clear-completed:hover { 323 | text-decoration: underline; 324 | } 325 | 326 | .info { 327 | margin: 65px auto 0; 328 | color: #bfbfbf; 329 | font-size: 10px; 330 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 331 | text-align: center; 332 | } 333 | 334 | .info p { 335 | line-height: 1; 336 | } 337 | 338 | .info a { 339 | color: inherit; 340 | text-decoration: none; 341 | font-weight: 400; 342 | } 343 | 344 | .info a:hover { 345 | text-decoration: underline; 346 | } 347 | 348 | /* 349 | Hack to remove background from Mobile Safari. 350 | Can't use it globally since it destroys checkboxes in Firefox 351 | */ 352 | @media screen and (-webkit-min-device-pixel-ratio:0) { 353 | .toggle-all, 354 | .todo-list li .toggle { 355 | background: none; 356 | } 357 | 358 | .todo-list li .toggle { 359 | height: 40px; 360 | } 361 | 362 | .toggle-all { 363 | -webkit-transform: rotate(90deg); 364 | transform: rotate(90deg); 365 | -webkit-appearance: none; 366 | appearance: none; 367 | } 368 | } 369 | 370 | @media (max-width: 430px) { 371 | .footer { 372 | height: 50px; 373 | } 374 | 375 | .filters { 376 | bottom: 10px; 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /todont/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | App 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /todont/index.js: -------------------------------------------------------------------------------- 1 | import {renderApp} from './ui' 2 | import {Fn, FnT} from '../lib/types' 3 | import {over, lensProp, remove, append} from 'ramda' 4 | const App = FnT(Fn) 5 | 6 | const L = { habits: lensProp('habits') } 7 | 8 | const Merge = x => 9 | ({ 10 | x, 11 | concat: other => 12 | Merge(Object.assign({}, x, other.x)) 13 | }) 14 | 15 | const create = App(habit => 16 | Fn.ask.map(over(L.habits, append(habit))) 17 | ).map(Merge) 18 | 19 | const destroy = App(({idx}) => 20 | Fn.ask.map(over(L.habits, remove(idx, 1))) 21 | ).map(Merge) 22 | 23 | const setShowPage = App.of({page: 'show'}).map(Merge) 24 | 25 | const setIndex = App(({idx}) => Fn.of({index: idx})).map(Merge) 26 | 27 | const view = setIndex.concat(setShowPage) 28 | 29 | const route = {create, destroy, view} 30 | 31 | const appLoop = (state) => 32 | renderApp(state, (action, payload) => 33 | appLoop( 34 | Merge(state) 35 | .concat(route[action].run(payload).run(state)) 36 | .x 37 | ) 38 | ) 39 | 40 | appLoop({page: 'list', habits: []}) -------------------------------------------------------------------------------- /todont/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todont", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "start": "webpack-dev-server --open" 7 | }, 8 | "devDependencies": { 9 | "@babel/core": "^7.1.0", 10 | "@babel/plugin-proposal-object-rest-spread": "^7.4.4", 11 | "@babel/preset-env": "^7.4.4", 12 | "@babel/preset-react": "^7.0.0", 13 | "babel-loader": "^8.0.5", 14 | "webpack": "^4.30.0", 15 | "webpack-cli": "^3.3.1", 16 | "webpack-dev-server": "^3.3.1" 17 | }, 18 | "browserslist": [ 19 | "Chrome 69" 20 | ], 21 | "dependencies": { 22 | "immutable": "^4.0.0-rc.12", 23 | "immutable-ext": "^1.1.5", 24 | "react": "^16.8.*", 25 | "react-dom": "^16.8.*", 26 | "ramda": "^0.26.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /todont/ui.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | const Item = ({name, onView, onDestroy}) => 5 |
  • 6 |
    7 | 10 |
    12 |
  • 13 | 14 | const Show = ({habit, dispatch}) => 15 |
    Showing {habit.name}
    16 | 17 | const List = ({habits, dispatch}) => 18 |
    19 | 23 | 28 |
    29 | 30 | const App = ({state, dispatch}) => 31 |
    32 |
    33 |

    Todont

    34 | e.key === 'Enter' ? dispatch('create', {name: e.currentTarget.value}) : undefined} 38 | autoFocus={true} 39 | /> 40 |
    41 |
    42 | {state.page === 'list' ? : } 43 |
    44 |
    45 | 46 | const renderApp = (state, dispatch) => { 47 | const app = 48 | ReactDOM.render(app, document.getElementById('app')) 49 | } 50 | 51 | export {renderApp} -------------------------------------------------------------------------------- /todont/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | 4 | module.exports = env => ({ 5 | mode: env === "production" ? "production" : "development", 6 | entry: { 7 | index: "./index.js" 8 | }, 9 | output: { 10 | filename: "./[name].bundle.js", 11 | path: path.resolve(__dirname) 12 | }, 13 | devServer: { 14 | contentBase: "./" 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.m?js$/, 20 | exclude: /node_modules(?!\/(@uxrd|salesforce))/, 21 | use: { 22 | loader: "babel-loader", 23 | options: { 24 | babelrc: false, 25 | presets: [ 26 | [ 27 | "@babel/preset-env", 28 | { 29 | targets: { chrome: "69" }, 30 | shippedProposals: true 31 | } 32 | ], 33 | "@babel/preset-react" 34 | ] 35 | } 36 | } 37 | } 38 | ] 39 | }, 40 | devtool: "cheap-module-source-map" 41 | }); 42 | -------------------------------------------------------------------------------- /transformers/0.js: -------------------------------------------------------------------------------- 1 | const { Task, Either } = require("../lib/types"); 2 | 3 | const Compose = (F, G) => { 4 | const M = fg => 5 | ({ 6 | extract: () => fg, 7 | map: f => M(fg.map(g => g.map(f))) 8 | }) 9 | M.of = x => M(F.of(G.of(x))) 10 | return M 11 | } 12 | 13 | const TaskEither = Compose(Task, Either) 14 | 15 | TaskEither.of(2) 16 | .map(two => two * 10) 17 | .map(twenty => twenty + 1) 18 | .extract() 19 | .fork(console.error, either => 20 | either.fold(console.log, console.log) 21 | ) 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /transformers/1.js: -------------------------------------------------------------------------------- 1 | const { TaskT, Task, Either, EitherT, IdT, Id } = require("../lib/types"); 2 | const { Left, Right } = Either 3 | const _ = require('lodash') 4 | 5 | const TaskEither = TaskT(Either) 6 | 7 | const users = [{id: 1, name: 'Brian'}, {id: 2, name: 'Marc'}, {id: 3, name: 'Odette'}] 8 | const following = [{user_id: 1, follow_id: 3}, {user_id: 1, follow_id: 2}, {user_id: 2, follow_id: 1}] 9 | 10 | const find = (table, query) => 11 | TaskEither.lift(Either.fromNullable(_.find(table, query))) 12 | 13 | const app = () => 14 | find(users, {id: 1}) // Task(Either(User)) 15 | .chain(u => find(following, {follow_id: u.id})) // Task(Either(User)) 16 | .chain(fo => find(users, {id: fo.user_id})) // Task(Either(User)) 17 | .fork(console.error, eu => 18 | eu.fold(console.error, console.log) 19 | ) 20 | 21 | app() 22 | 23 | 24 | -------------------------------------------------------------------------------- /transformers/2.js: -------------------------------------------------------------------------------- 1 | const { Fn, FnT, TaskT, Task, Either, EitherT, Id, IdT } = require("../lib/types"); 2 | 3 | const EitherId = EitherT(Id) 4 | const App = FnT(EitherId) 5 | 6 | App 7 | .of(2) 8 | .chain(two => App.lift(EitherId.of(two + two))) 9 | .chain(four => App.lift(EitherId.lift(Id.of(four)))) 10 | .map(four => four + 1) 11 | .run() 12 | .fold(console.error, fx => console.log(fx.extract())) --------------------------------------------------------------------------------