Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Strawman proposal for providing Fantasyland Stream type in a separate package #675

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ jobs:
name: Install Local JavaScript Dependencies
command: |
nvm use default
npm install
npm install --legacy-peer-deps

# Store a cache of local JavaScript modules.
- save_cache:
Expand Down Expand Up @@ -111,7 +111,7 @@ jobs:
name: Install Local JavaScript Dependencies
command: |
nvm use node
npm install
npm install --legacy-peer-deps

# Store a cache of local JavaScript modules.
- save_cache:
Expand Down
9 changes: 9 additions & 0 deletions packages/core-fl/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
node_modules/
.git/
.idea/
examples/
benchmark/
experiments/
perf/
test/perf/
dist/
41 changes: 41 additions & 0 deletions packages/core-fl/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"author": "[email protected]",
"description": "Fantasy-Land Api for @most/core",
"exports": {
".": {
"import": "./dist/index.es.js",
"require": "./dist/index.js"
}
},
"files": [
"type-definitions",
"dist"
],
"license": "MIT",
"main": "dist/index.js",
"module": "dist/index.es.js",
"name": "@most/core-fl",
"scripts": {
"build:dist": "rollup -c",
"test": "mocha --require ts-node/register ./test/*test**"
},
"type": "module",
"version": "1.0.0",
"dependencies": {
"@most/disposable": "^1.3.0",
"@most/prelude": "^1.8.0",
"@most/scheduler": "^1.3.0",
"@most/types": "^1.1.0"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-typescript": "^11.1.6",
"@types/ramda": "^0.29.10",
"fp-ts": "^2.16.2",
"mocha": "^10.3.0",
"ramda": "^0.29.1",
"rollup": "^4.10.0",
"rollup-plugin-typescript2": "^0.36.0",
"typescript": "^5.3.3"
}
}
37 changes: 37 additions & 0 deletions packages/core-fl/rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// import typescript from '@rollup/plugin-typescript'
import typescript from 'rollup-plugin-typescript2'
import { nodeResolve } from '@rollup/plugin-node-resolve'
import pkg from './package.json' assert { type: 'json' }

export default {
input: 'src/index.ts',
plugins: [
nodeResolve({
extensions: ['.mjs', '.js', '.json', '.node']
}),
typescript()
],
external: [
'@most/scheduler',
'@most/disposable',
'@most/prelude'
],
output: [
{
file: pkg.main,
format: 'umd',
name: 'mostCoreFl',
sourcemap: true,
globals: {
'@most/scheduler': 'mostScheduler',
'@most/disposable': 'mostDisposable',
'@most/prelude': 'mostPrelude'
}
},
{
file: pkg.module,
format: 'es',
sourcemap: true
}
]
}
58 changes: 58 additions & 0 deletions packages/core-fl/src/FantasyLandStream.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Stream, Sink, Scheduler, Disposable } from '@most/types'
import {
continueWith,
empty,
filter,
map,
ap,
now,
chain,
never
} from '../../core'

interface FunctorFantasyLand<A> {
['fantasy-land/map']<B>(fn: (a: A) => B): FunctorFantasyLand<B>
}

export const fantasyLand = <A>(stream: Stream<A>): FantasyLandStream<A> =>
new FantasyLandStream(stream)

export class FantasyLandStream<A> implements Stream<A>, FunctorFantasyLand<A> {
constructor(private readonly stream: Stream<A>) {}

run(sink: Sink<A>, scheduler: Scheduler): Disposable {
return this.stream.run(sink, scheduler)
}

['fantasy-land/concat'](nextStream: Stream<A>): FantasyLandStream<A> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's probably some discussion to be had on what a proper stream monoid should be. There might even be more than 1 valid monoid instance.

The way you've used continueWith here is nice. It's like a simple switch. I've come to believe that merge is also a pretty good semigroup.

Any thoughts or insights on which might be the better default semigroup instance? Are there others? See my other, related comment about zero as well.

return fantasyLand<A>(continueWith(() => nextStream, this.stream))
}

['fantasy-land/empty']<B>() {
return fantasyLand<B>(empty())
}

['fantasy-land/filter'](predicate: (value: A) => boolean) {
return fantasyLand<A>(filter(predicate, this.stream))
}

['fantasy-land/map']<B>(fn: (value: A) => B): FantasyLandStream<B> {
return fantasyLand<B>(map(fn, this.stream))
}

['fantasy-land/ap']<B>(mapper: Stream<(value: A) => B>) {
return fantasyLand<B>(ap(mapper, this.stream))
}

['fantasy-land/of']<B>(value: B) {
return fantasyLand<B>(now(value))
}

['fantasy-land/chain']<B>(fn: (value: A) => Stream<B>) {
return fantasyLand<B>(chain(fn, this.stream))
}

['fantasy-land/zero']() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way I read the FL spec, @more/core-fl would also need to implement alt. Is that right?

It seems like you'll need to ensure that concat+empty are coherent, as are alt+zero. Have you thought about what makes sense for those in terms of coherence and their laws?

return fantasyLand<A>(never())
}
}
22 changes: 22 additions & 0 deletions packages/core-fl/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { fantasyLand, FantasyLandStream } from './FantasyLandStream'
import { compose, curry2 } from '@most/prelude'
import { Stream } from '@most/types'

import { periodic as _periodic, take as _take, tap as _tap } from '../../core/src'

export const periodic = compose(fantasyLand, _periodic)

interface Take {
<A>(n: number, s: Stream<A>): FantasyLandStream<A>
<A>(n: number): (s: Stream<A>) => FantasyLandStream<A>
}
export const take: Take = curry2((x, y) => fantasyLand(_take(x, y)))

interface Tap {
<A>(f: (a: A) => any, s: Stream<A>): FantasyLandStream<A>
<A>(f: (a: A) => any): (s: Stream<A>) => FantasyLandStream<A>
}
export const tap: Tap = curry2((x, y) => fantasyLand(_tap(x, y)))

export { fantasyLand, FantasyLandStream }
export { runEffects } from '../../core'
35 changes: 35 additions & 0 deletions packages/core-fl/test/fantasy-land-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, it } from 'mocha'
import { assert } from '@briancavalier/assert'
import { newDefaultScheduler } from '@most/scheduler'
import { Stream } from '@most/types'

import { periodic, runEffects, take, tap, FantasyLandStream } from '../src/index'
import { pipe } from 'fp-ts/function'
import { map as mapR } from 'ramda'

const map = mapR as unknown as <A, B>(fn: (x: A) => B) => (functor: FantasyLandStream<A>) => FantasyLandStream<B>

const defaultScheduler = newDefaultScheduler()
const runEff = <A>(s: Stream<A>): Promise<void> => runEffects(s, defaultScheduler)

describe('fantasy-land', function () {
// const x = [0, 1, 2, 3]
// const sampleError = new Error('sample error')

it('the types should line up', function () {
return runEff(take(2, periodic(10)))
.then(res => {
assert(typeof res === 'undefined')
})
})

it('map', () => pipe(
periodic(10),
take(2),
map(() => 'foo'),
tap(x => {
assert(x === 'foo')
}),
runEff
))
})
11 changes: 11 additions & 0 deletions packages/core-fl/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"declarationDir": "./dist",
"target": "ES2020"
},
"extends": "../../tsconfig",
"include": [
"src",
"test"
]
}
2 changes: 1 addition & 1 deletion packages/core/src/combinator/continueWith.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class ContinueWithSink<A, B> extends Pipe<A, A | B> implements Sink<A>, Disposab
try {
this.disposable = this.continue(this.f, t, sink)
} catch (e) {
sink.error(t, e)
sink.error(t, e as Error)
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/combinator/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ class RecoverWithSink<A, E extends Error, B> implements Sink<A>, Disposable {
try {
this.disposable = this._continue(this.f, t, x, sink)
} catch (e) {
sink.error(t, e)
sink.error(t, e as Error)
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/combinator/mergeConcurrently.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ class Outer<A, B> implements Sink<A>, Disposable {
try {
this.initInner(t, x)
} catch (e) {
this.error(t, e)
this.error(t, e as Error)
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/runEffects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ function tryDispose <X>(error: (e: Error) => void, end: (x: X) => void, x: X, di
try {
disposable.dispose()
} catch (e) {
error(e)
error(e as Error)
return
}

Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/source/tryEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ export function tryEvent <A>(t: Time, x: A, sink: Sink<A>): void {
try {
sink.event(t, x)
} catch (e) {
sink.error(t, e)
sink.error(t, e as Error)
}
}

export function tryEnd(t: Time, sink: Sink<unknown>): void {
try {
sink.end(t)
} catch (e) {
sink.error(t, e)
sink.error(t, e as Error)
}
}
2 changes: 1 addition & 1 deletion packages/core/src/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ export function runTask <E, A>(task: DeferrableTask<E, A>): E | A {
try {
return task.run()
} catch (e) {
return task.error(e)
return task.error(e as Error)
}
}