Skip to content

Instantly share code, notes, and snippets.

@Lucifier129
Created July 13, 2022 08:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Lucifier129/bd3bbeab98ed59db7b8e513cc02eea14 to your computer and use it in GitHub Desktop.
Save Lucifier129/bd3bbeab98ed59db7b8e513cc02eea14 to your computer and use it in GitHub Desktop.
Modular state-management via codata & lens & state-machine
type PrimitiveData = string | number | boolean | null
type ListData = LensData[]
type ObjectData = {
[key: string]: LensData
}
type LensData = PrimitiveData | ListData | ObjectData
type Setter<data, source = data> = (data: data) => source
type Getter<data, source = data> = (source: source) => data
type Accessor<data, source = data> = {
get: Getter<data, source>
set: Setter<data, source>
}
type SelectByKey<data extends ObjectData, source> = <key extends keyof data>(key: key) => Lens<data[key], source>
type SelectByIndex<data extends ListData, source> = (index: number) => Lens<data[number], source>
type Select<data, source> = data extends ObjectData
? {
select: SelectByKey<data, source>
}
: data extends ListData
? {
select: SelectByIndex<data, source>
}
: {}
type LensActions<data, source = data> = Accessor<data, source> & Select<data, source>
type Lens<data, source = data> = [data, LensActions<data, source>]
const isObjectData = (value: unknown): value is ObjectData => typeof value === 'object' && value !== null
const isListData = (value: unknown): value is ListData => Array.isArray(value)
const isPrimitiveData = (value: unknown): value is PrimitiveData =>
typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || value === null
const ListLens = <data extends ListData, source = data>(data: data, { get, set }: Accessor<data, source>) => {
const select: SelectByIndex<data, source> = (index) => {
return Lens<data[number], source>(data[index], {
get: (source) => get(source)[index] as data[number],
set: (item) => {
if (item === data[index]) {
return set(data)
}
const newData = [...data] as data
newData[index] = item
return set(newData)
},
})
}
return [data, { get, set, select }] as unknown as Lens<data, source>
}
const ObjectLens = <data extends ObjectData, source = data>(data: data, { get, set }: Accessor<data, source>) => {
const select: SelectByKey<data, source> = (key) => {
return Lens<data[typeof key], source>(data[key], {
get: (source) => get(source)[key],
set: (value) => {
if (value === data[key]) {
return set(data)
}
const newData = { ...data }
newData[key] = value
return set(newData)
},
})
}
return [data, { get, set, select }] as unknown as Lens<data, source>
}
const Lens = <data extends LensData, source = data>(
data: data,
accessor: Accessor<data, source>,
): Lens<data, source> => {
if (isListData(data)) {
return ListLens(data, accessor as unknown as Accessor<ListData, source>) as unknown as Lens<data, source>
}
if (isObjectData(data)) {
return ObjectLens(data, accessor as unknown as Accessor<ObjectData, source>) as unknown as Lens<data, source>
}
if (isPrimitiveData(data)) {
return [data, accessor] as Lens<data, source>
}
throw new Error(`Unsupported data type: ${data}`)
}
const identity = <x>(x: x): x => x
const from = <data extends LensData>(data: data): Lens<data> => {
return Lens(data, {
get: identity,
set: identity,
})
}
const TextMachine = <source>([text, { set }]: Lens<string, source>) => {
const setText = (value: string) => {
return set(value)
}
const clearText = () => {
return set('')
}
return [text, { setText, clearText }] as const
}
const SwitchMachine = <data, source>([data, { set }]: Lens<data, source>) => {
const switchTo = (value: data) => {
return set(value)
}
return [data, { switchTo }] as const
}
const ToggleMachine = <source>(lens: Lens<boolean, source>) => {
const [value, { switchTo }] = SwitchMachine(lens)
const toggle = () => {
return switchTo(!value)
}
return [value, { toggle }] as const
}
type TodoData = {
id: string
title: string
completed: boolean
}
const TodoMachine = <source>([todo, { select }]: Lens<TodoData, source>) => {
const [, { setText, clearText }] = TextMachine(select('title'))
const [, { toggle }] = ToggleMachine(select('completed'))
return [
todo,
{
setTitle: setText,
clearTitle: clearText,
toggleCompleted: toggle,
},
] as const
}
let todoUid = 0
const TodoListMachine = <source>([todos, { set, select }]: Lens<TodoData[], source>) => {
const getTodoActions = (todoId: string) => {
const index = todos.findIndex((todo) => todo.id === todoId)
if (index === -1) {
throw new Error(`Todo with id ${todoId} not found`)
}
return TodoMachine(select(index))[1]
}
const addTodo = (title: string) => {
const newTodo = {
id: `${todoUid++}`,
title,
completed: false,
}
return set([...todos, newTodo])
}
const removeTodo = (todoId: string) => {
const index = todos.findIndex((todo) => todo.id === todoId)
if (index === -1) {
throw new Error(`Todo with id ${todoId} not found`)
}
const newTodos = [...todos]
newTodos.splice(index, 1)
return set(newTodos)
}
return [todos, { addTodo, removeTodo, getTodoActions }] as const
}
type TodoFilter = 'all' | 'active' | 'completed'
type TodoAppState = {
todoInput: string
todos: TodoData[]
todoFilter: TodoFilter
}
const TodoAppMachine = <source>([state, { get, set, select }]: Lens<TodoAppState, source>) => {
const [, inputActions] = TextMachine(select('todoInput'))
const [, todosActions] = TodoListMachine(select('todos'))
const [, todoFilterActions] = SwitchMachine(select('todoFilter'))
const addTodo = () => {
const newTodos = get(todosActions.addTodo(state.todoInput)).todos
const newTodoInput = get(inputActions.clearText()).todoInput
return set({
...state,
todoInput: newTodoInput,
todos: newTodos,
})
}
return [
state,
{
addTodo,
todoInput: inputActions,
todos: todosActions,
todoFilter: todoFilterActions,
},
] as const
}
const initialState: TodoAppState = {
todoInput: '',
todos: [],
todoFilter: 'all',
}
let [state, actions] = TodoAppMachine(from(initialState))
console.log('initial-state', state)
;[state, actions] = TodoAppMachine(from(actions.todoInput.setText('Hello')))
console.log('set todo input', state)
;[state, actions] = TodoAppMachine(from(actions.addTodo()))
console.log('add todo', state)
;[state, actions] = TodoAppMachine(from(actions.todoInput.setText('World')))
console.log('set todo input', state)
;[state, actions] = TodoAppMachine(from(actions.addTodo()))
console.log('add todo', state)
;[state, actions] = TodoAppMachine(from(actions.todoFilter.switchTo('active')))
console.log('switch todo filter', state)
;[state, actions] = TodoAppMachine(from(actions.todos.getTodoActions('0').toggleCompleted()))
console.log('toggle todo', state)
;[state, actions] = TodoAppMachine(from(actions.todos.getTodoActions('0').toggleCompleted()))
console.log('toggle todo', state)
;[state, actions] = TodoAppMachine(from(actions.todos.getTodoActions('0').setTitle('Hello World_update')))
console.log('set todo title', state)
;[state, actions] = TodoAppMachine(from(actions.todos.removeTodo('0')))
console.log('remove todo', state)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment