Prototype lifecycle

This commit is contained in:
Eric Rumsey 2025-05-29 17:46:26 -05:00
parent e220b7930c
commit 450745011a
2 changed files with 124 additions and 57 deletions

View File

@ -1,34 +1,26 @@
import { type CursorPos } from 'node:readline' import { type CursorPos } from 'node:readline'
import { type Either, fold, type Left, left, right } from '../util/basic/either' import { type Either, left, right } from '../util/basic/either'
import { hasTag, type Tag } from '../util/basic/utility'
import { PaintCursorError } from './errors' import { PaintCursorError } from './errors'
// Cursor stuffs export interface CursorPosAxis extends Tag<'X' | 'Y'> {
interface CursorPosX {
readonly _tag: 'X'
readonly value: number readonly value: number
} }
interface CursorPosY { type CursorPosVal = (tag: 'X' | 'Y') => (x: number) => CursorPosAxis
readonly _tag: 'Y' export const cursorPosVal: CursorPosVal = tag => x => ({_tag: tag, value: x})
readonly value: number
}
type CursorPosVal = (str: 'X' | 'Y') => (x: number) => CursorPosX | CursorPosY export const cursorPos = (x: number) => (y: number): CursorPos => ({
const cursorPosVal: CursorPosVal = str => x => ({_tag: str, value: x}) rows: x,
cols: y,
})
type IsCursorPos = (x: 'X' | 'Y') => (y: CursorPosX | CursorPosY) => boolean
const isCursorPos: IsCursorPos = x => y => y._tag === x
const cursorPos = (x: number) => (y: number): CursorPos => ({rows: x, cols: y})
// Cursor Position
type SafeCursorPos = type SafeCursorPos =
(x: CursorPosX | CursorPosY) => (x: CursorPosAxis) => (y: CursorPosAxis) => Either<CursorPos, Error>
(y: CursorPosX | CursorPosY) => Either<CursorPos, Error> export const safeCursorPos: SafeCursorPos = (x) => y => {
const safeCursorPos: SafeCursorPos = (x) => y => { return hasTag('X')(x) && hasTag('Y')(y)
return isCursorPos('X')(x) && isCursorPos('Y')(y)
? right(cursorPos(x.value)(y.value)) ? right(cursorPos(x.value)(y.value))
: isCursorPos('Y')(x) && isCursorPos('X')(y) : hasTag('Y')(x) && hasTag('X')(y)
? right(cursorPos(y.value)(x.value)) ? right(cursorPos(y.value)(x.value))
: left( : left(
new PaintCursorError( new PaintCursorError(
@ -38,56 +30,30 @@ const safeCursorPos: SafeCursorPos = (x) => y => {
) )
} }
console.log( export interface CanvasOrigin extends Tag<'Origin'> {
safeCursorPos(cursorPosVal('X')(5))(cursorPosVal('X')(7)),
)
// Tagged types
interface CanvasOrigin {
readonly _tag: 'Origin'
readonly value: CursorPos readonly value: CursorPos
} }
interface CanvasEnd { export interface CanvasEnd extends Tag<'End'> {
readonly _tag: 'End'
readonly value: CursorPos readonly value: CursorPos
} }
type CanvasOriginConstructor = (x: SafeCursorPos) => Either<CanvasOrigin, E> export const origin = (pos: CursorPos): CanvasOrigin => ({
const canvasOrigin = (x: CursorPos): CanvasOrigin => ({
_tag: 'Origin',
value: x,
})
const canvasEnd = (x: CursorPos): CanvasEnd => ({
_tag: 'End',
value: x,
})
// Canvas type using tagged positions
type Canvas = {
origin: CanvasOrigin,
end: CanvasEnd,
}
// Constructor helpers
const origin = (pos: CursorPos): CanvasOrigin => ({
_tag: 'Origin', _tag: 'Origin',
value: pos, value: pos,
}) })
const end = (pos: CursorPos): CanvasEnd => ({ export const end = (pos: CursorPos): CanvasEnd => ({
_tag: 'End', _tag: 'End',
value: pos, value: pos,
}) })
// Curried canvas creator with type safety export interface Canvas {
const canvas = (origin: CanvasOrigin) => (end: CanvasEnd): Canvas => ({ origin: CanvasOrigin
end: CanvasEnd
}
export const canvas = (origin: CanvasOrigin) => (end: CanvasEnd): Canvas => ({
origin, origin,
end, end,
}) })
// Usage with proper type safety
const myCanvas = canvas(origin({rows: 3, cols: 1}))(
end({rows: 8, cols: 1}),
)

View File

@ -0,0 +1,101 @@
import { type CursorPos } from 'node:readline'
import type { Either } from '../util/basic/either'
import {
DoOption,
getOrElse,
none,
type Option,
some,
} from '../util/basic/option'
import { hasTag, pipe, type Tag } from '../util/basic/utility'
import {
type CanvasEnd,
type CanvasOrigin,
cursorPos,
end,
origin,
} from './canvas'
interface PainterExit extends Tag<'PainterExit'> {}
interface PainterTask<T extends object = {}> extends Tag<'PainterTask'> {
task: {
origin: CanvasOrigin,
end: CanvasEnd,
effect: PainterEffect<T & Tag<'PainterState'>>,
state: Option<T & Tag<'PainterState'>>,
}
}
type TaskConstructor<T extends object = {}> = (
x: CursorPos,
y: CursorPos,
) => (f: PainterEffect, state?: T & Tag<'PainterState'>) => PainterTask<T>
const task: TaskConstructor = (x, y) => (f, state?) => {
return {
_tag: 'PainterTask',
task: {
origin: origin(x),
end: end(y),
effect: f,
state: !state ? none : some(state),
},
}
}
type PainterEffect<
T extends object = {},
> = (task: PainterTask<T>) => Either<PainterTask<T>, PainterExit>
type EffectState = <T extends object = {}>(x: T) => T & Tag<'PainterState'>
const effectState: EffectState = x => ({_tag: 'PainterState', ...x})
export { type PainterEffect, task }
//
// testing
//
type TestEffect = (x: PainterTask<{message: Option<string>}>) => PainterTask<
{message: Option<string>}
>
const testEffect: PainterEffect<{message: string, counter: number}> = x => {
const state = DoOption.start()
.bind('state', x.task.state)
.doL(ctx => {
console.log('Message:', ctx.state.message)
return some(undefined)
})
.doL(ctx => {
console.log('Counter:', ctx.state.counter)
return some(undefined)
})
.return(ctx =>
(ctx.state.counter < 100000)
? {...ctx.state, counter: ctx.state.counter + 1}
: none
)
if (pipe(state, getOrElse(none), hasTag('None'))) return {_tag: 'PainterExit'}
return task(x.task.origin.value, x.task.end.value)(
x.task.effect,
getOrElse(none)(state),
)
}
const textTask = task<{message: string, counter: number}>(
cursorPos(2)(0),
cursorPos(4)(0),
)(
testEffect,
{
_tag: 'PainterState',
message: 'test message',
counter: 1,
},
)
const testFn = (x: PainterTask | PainterExit) => {
return hasTag('PainterExit')(x) ? 'Exit' : testFn(x.task.effect(x))
}
testFn(textTask)