-
Notifications
You must be signed in to change notification settings - Fork 59
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
Reviewing Monio's design/behavior (formerly: Typescript Support) #8
Comments
I do not have any plans to convert to TS. Internally, there are so many layers of value polymorphism to make it both lawful and ergonomic, it seems like it would a real challenge to type all those boundaries. Just curious: how would the implementation being in TS or not affect users of the library? However, I would entertain |
I've forked the project and started working on the conversion (in my free time). But I'm currently labeling the conversion as a pet project, so don't assign it to me just yet 😅 |
@getify I'm working on the implementation, but I don't understand the purpose of the Why does In other words:
P.S. |
@getify Also, what is the purpose of the |
Thanks for looking into this effort! Appreciate the help. :)
Summoning @DrBoolean for further comment if I'm unclear in any of my answers here. The
As far as I can tell, that's a violation of the lawful usage, because Just.of([1,2]).concat(Just.of([3])._inspect(); // Just([1,2,3])
That's what concatable/semigroup does... it sorta delegates to an underlying value's It only appears to be calling against the items of the array because you passed an array to I think another way of saying this is: unfortunately, while JS arrays (and strings) are concatable/semigroups, they're not conforming monads.
I indeed may have mistakes in Monio, I didn't claim it's perfect. However, in this case, I think you'll find it's consistent when the methods are used lawfully. That is, methods like
All of the monads in Monio expose public
|
I got confused because of the test in qunit.test("#concat", (assert) => {
assert.deepEqual(
just.of([1, 2]).concat([3]),
[[1, 2, 3]],
"should concat a just array to an array"
);
}); |
Sorry, that's a mistake... those tests came from another contributor and I didn't look closely enough. |
👋 Nailed it |
@getify After a few months of hiatus, I returned to this conversion and had some more questions:
|
Thanks for picking back up the effort, welcome back! :) Your questions all directly flow from type/category theory -- that is, none of that is particularly specific to Monio. I'm no expert on the topic, but I think I'm competent enough to give it a stab (again, with review from @DrBoolean). Brace yourself, because this is a looooong explanation. :)
As applied to Either and Maybe, it's essentially reducing (or, unwrapping) the duality choice inherent in those monads, such that you get the underlying value. It does so by "deciding" which of two functions to invoke (and provide the value). For Maybe, it invokes the first function in the case of Maybe:Nothing, and the second function in the case of Maybe:Just. For Either, it invokes the first function in the case of Either:Left and the second function in the case of Either:Right. This function is somewhat special (in my perspective) in that the end result of calling it is extracting the underlying value, as opposed to requiring that it produce another Monad of the same kind as
The It's perhaps more easily illustrated in code, like this (using a curried const sum = x => y => x + y;
var threePlus = Just( sum(3) ); // Just( y => x + y ) ... IOW, a Just holding a unary function
var four = Just(4); // a Just holding a single numeric value
var seven = threePlus.ap( four ); // Just(7)
// which is the same as (the not quite lawful but still illustrative):
var three = Just(3);
var seven = four.map( three.chain(sum) ); // Just(7) That last line isn't "lawful" in that the Since that kind of transformation is possible but not lawful, the To your original question, Moreover, since the functions in IOs are for the laziness of IO, and thus aren't typically part of the operations/transformations themselves, to make var x = IO( () => v => v * 3 ); // IO holding a simple unary function
var y = IO( fn => fn(7) * 2 ); // IO as a HOF
var z = y.ap(x); // IO essentially holding the lambda function () => 42
z.run(); // 42 Actually, TBH, I'm not even positive IO's implementation of
Not quite. Again, the "lawful" use of
Similar, yes. Equivalent (interchangable)? No.
I think that would be challenging/unwise for a few reasons, mostly implementation related to Monio, but I think in theory it would be possible to do so lawfully, though you couldn't do so the other way around (since Nothing is lossy -- holds no value -- and Left holds a distinct value). One challenge is the internal type (aka "kind") checking I do with That's mostly done because typically Just and Nothing are not implemented as separate standalone monads, but rather as directly tied to Maybe. Monio did that because it makes illustrating monad concepts easy when you have, essentially, the identity monad with OTOH, Either:Left / Either:Right are deeply tied to Either, and don't exist in Monio standalone. So I can't really see how Just / Nothing could be cleanly implemented on top of them, without separating them from Either -- which there's no real reason to do, since they're not particularly useful as standalone monads. Also, the Nothing monad is different from the Either:Left monad, from a performance perspective, because its short-circuiting is to always return the public Nothing interface (since we don't need to hold any instance value in closure). By contrast, the short-circuiting that Either:Left does is instance-sensitive (which is slightly less performant than what Nothing does). (deep breath) Phew, hope that was helpful. Hope I didn't make things worse by confusing or overwhelming the situation. Bottom line: just about everything you'll encounter in Monio's monads is there for a reason -- generally for some aspect of type or category theory. Only some of that space is personally in my comfort zone, but Monio is designed to pull together a lot of those things into a set of all-powerful monadic goodness. ;-) |
I should also mention that while I endeavor to maintain "lawful" uses and avoid "unlawful but possible" uses, internally there are definitely cases where I intentionally violate the laws because it's more performant or more convenient with my implementation. For example, the My point is, while the TS typings can be aware of these lawful vs unlawful usages, we shouldn't design them such that they restrict such usages. |
That was helpful. I had a hunch on most of the stuff you explained, but wanted to make sure instead of making assumptions. Putting aside my TS implementation I think you can simplify the function inspect(val?: unknown): string {
if (typeof val === "string") return `"${val}"`;
if (typeof val == "undefined") return String(val);
if (typeof val === "function") return val.name || "anonymous function";
if (val instanceof Either) {
return val.fold(
(v) => "Left(${inspect(v)})",
(v) => `Just(${inspect(v)})`,
);
}
if (val instanceof Maybe) {
return val.fold(
() => "Nothing",
(v) => `Just(${inspect(v)})`,
);
}
if (Array.isArray(val)) return `[${val.map(inspect).join(", ")}]`;
if (val instanceof Object && val.constructor === Object) {
return JSON.stringify(val);
}
return String(val);
} |
@getify Expanding on your previous answer, is it true that the |
I'm not quite sure what you're suggesting. Is this a single central Or are you suggesting that each monad kind would call this central util from their own respective Some things to be aware of in the current implementation of the various inspect functions:
Those concerns aside, are there other reasons you think inspect should be centralized/shared rather than per-kind as it currently is? In your opinion, how does that "simplifiy" compared to the current approach? |
I'm suggesting a single |
Yes, and no.
I would like the TS typings to "encourage" (aka "should") these matching, without "requiring" (aka "must") it. I don't know how easily that sort of thing can be communicated by a type system -- not my area of specialty. But in essence, this is one of the reasons Monio wasn't built as a strictly-typed FantasyLand-compliant library, because when doing JS, I feel there are times when you need to intentionally color outside the lines.
Other methods that are commonly found on the monad kinds' APIs, such as |
I'm sorry, I'm confused by what this is? |
|
It's true that in modern JS engines, My concern with classes and I'll elaborate on my feelings about classes to illuminate that stance: I hate littering the internals of the code with As a user, I having to put The function (factory) approach chosen by Monio elects to use closure to maintain (and protect) state internally, rather than state being exposed on public instance properties in classes. If all state is public, it can be mucked with (accidentally or intentionally), so you have less certainty. Whereas, if state is private via closure, you get protection and more certainty by default. Yes, All of that "cost" of choosing But I guess the most compelling reason to prefer functions+closure of So, if we shouldn't encourage users of Monio to use |
@getify I'm not sure I agree with the first part of your comment (about your distaste for classes in JS). But I agree that if this library provides Monads (a |
@getify Another question: |
In your example, the result would be |
So proper usage of Another question is about the implementation of Also, I've read your function is(val) {
return typeof val === 'object' && !!val && val['brand'] === brand;
} The "vulnerability" is that someone could just place a function called P.S> Thank you for taking the time and explaining all of that. As you see, I don't have a lot of experience with Monads. |
This is an ergonomic affordance from Monio, specifically. The monad laws require a "unit constructor" that just wraps, so you could intentionally create a This special helper constructor is an artifact of Monio's choice to have Since Either doesn't expose Left and Right as standalone monads, it has no need for lifting a Right to be an Either:Right, and thus no need for a distinction between the |
I've debated this approach quite a bit in the design/evolution of Monio. I initially just had a publicly exposed property (that was writable). Then I decided to make it a read-only, non-enumerable property, to make it less likely that it would be mucked with or faked. Then I realized those techniques were more "OO" than functional, so I switched to using the closure over a private brand for the identity checking. It's not perfect, but it's a lot more likely to "work as expected" than the public writable property. Of course, it relies on the public method (aka property) There's a much more fool-proof way of doing this, which is to track instances in hidden WeakMaps. I contemplated doing that, but it seems way overkill. It also has a (very slight) disadvantage: instances can't be shared across Realms (like from main page to an iframe, for example). So bottom line, the current compromise is to favor expressing the branding check as a function since functions are sanctified in FP as being constant (even though they aren't, by default). I think that provides more protection than the simpler approaches, but doesn't take it too far. |
(btw, I've edited this thread topic to more accurately reflect what it's become: a general review of all the design/implementation decisions made in Monio thus far, for posterity sake) |
|
I briefly considered it, yes, at the same time (as mentioned) I was experimenting with the read-only/non-enumerable properties. It seemed like overkill, and more along the lines of how OO classes identify themselves, as opposed to how plain ol' objects with closure'd methods work. I decided, and still think, the
Understood. Again, I went down a similar route early on, but ultimately decided instead of trying to protect it through various means, I would just make it clear that the value was off-limits by hiding it behind a function closure. Anyone doing FP should recognize that and respect it. If they don't, they deserve whatever code breakage they get themselves into. I don't want code weight or performance costs to keep their hands off it. Closure gives me what I want pretty much for free.
Any of them, depending on their needs! If they want a monadic unit constructor, their best option is For Just, Also note that Maybe has another helper which is not a unit-constructor, but is quite common and useful (I use it all the time): Similarly, Either has
Not quite. And in practice, you'd probably never actually manually make Nothing instances, but it IS possible you might use it as an "empty" value in a reduction/fold or something like that. Ultimately, there was no reason in my mind not to have Just and Nothing be separate, since at a minimum they're very convenient for illustrating monad principles on, without any other fanfare. And in some specific cases, they may actually prove useful. Either way, in those cases you'd want the most stripped down and predictable thing, not some more powerful Maybe value that comes with other behavioral characteristics. |
On an unrelated note, I see that there is no handling for invalid input in case of the For example: function ap(m: Monad<GetParam<R>>): Monad<GetReturn<R>> | Either<L, R> {
if (isLeft(lr)) return publicAPI;
if (!isFunc<GetParam<R>, GetReturn<R>>(lr.val)) {
throw new TypeError("Not a function");
}
return m.map(lr.val);
} Note 1: |
In general, there's very little nagging if you pass the "wrong" inputs. This is on purpose, in part to keep file size down and performance lean, and in part to offer flexibility in case you know what you're doing and need to color outside the lines. The types system doesn't necessarily have to be as flexible as that, but as I've mentioned earlier, it's a known scenario that a function like While we're discussing these gray areas, does TS offer some idea like defining types in multiple levels of strictness? Like, for example, could a project like Monio have a set of types that are very strict, and another set of types that are a bit more flexible, and then users can choose to opt into one set of types or another? If that kind of thing is possible, somehow, I think it would be best for Monio. You just won't get the most out of Monio if your proclivity is to absolutely type single bit and strictly and narrowly adhere to those, because that's not the philosophy I'm using to design this library. |
|
@getify I'm trying to figure out how to translate the const main = IO.do(({window: win}) => {
yield IO(({window: win}) => window.console.log('hello'))
// ...
yield IO(({window: win}) => window.console.log('world'))
});
main.run({window}) Is it true that the input for each yielded IO will always be the value inserted into If not, then when and how does it change? |
Inside a "do routine", an expression like I say "kinda" because if the expression isn't an IO, but is instead a promise, then the promise is waited on to unwrap its value. If it's a Maybe or Either, it's also unwrapped (and short-circuits out if need be). And if it's an IO, and the So by default, a Reader env propagates all the way down (sorta recursively) through all chained IO/Readers... unless you forcibly override it (which I often do, to narrow/alter the applicable env further down the call chain). IO.do(function *main(env1){
yield IO(() => IO.do(another).run({
x: env1.x + 1,
y: 3
}));
})
.run({ x: 1 });
function *another(env2) {
console.log(env2.x,env2.y);
// 2 3
} |
Yes, that's what I thought. Though, it seems redundant to pass the "real world" to both the generator function and to the IO Yields. Isn't it better to only pass it to the IO Yields, as a way of saying, if you want access to the "real world", then you have to use an IO operation? By passing the "real world" to the generator function, you open the door for developers to use it outside of an IO operation: IO.do(function *main(env1){
yield IO.do(another).run({
x: env1.x + 1,
y: 3
});
})
.run({ x: 1 });
function *another(env2) {
console.log(env2.x,env2.y);
// 2 3
} It seems like I'm using the "real world" in an IO operation, but I'm not. However, if the "real world" was passed only to IO operation, the above example will obviously not work, and by accessing global variables, it becomes obvious that they don't use monadic principles. IO.do(function *main(){
yield IO((env1) => IO.do(another).run({
x: env1.x + 1,
y: 3
}));
})
.run({ x: 1 });
function *another(env2) {
console.log(env2.x,env2.y);
// 2 3
} |
There's a variety of reasons the Reader env is passed into both the IO function and the do-routine. First, understand... a do-routine itself is a significant readbility/convenience affordance, which in essence communicates "everything here is a (potential) side effect". I think typically you assume everything in a do-routine is a side-effect unless otherwise noted. In that context, there's not particularly any strong reason to have to make things (like accessing and making side effects) more difficult inside the do-routine, since that defeats the purpose of using it in the first place. For example, imagine I was going to do this: IO.do(function *main(){
var btn = yield getElement("#my-btn");
btn.disabled = false; // this line is a side effect!
}); The first line uses a IO.do(function *main(){
var btn = yield getElement("#my-btn");
yield IO(() => btn.disabled = false);
}); Sure, of course you can. And if you find yourself doing that a lot, you can make a helper... IO.do(function *main(){
var btn = yield getElement("#my-btn");
yield disableElement(btn);
}); But for the one-off situations, do we need to force ourselves into a discipline of having every single side-effect line in a do-routine being wrapped in an IO? This is ultimately a judgement call. The whole reason you dropped out of an IO chain into a do-routine was because you wanted some readability affordances, like being able to assign temporary local variables to carry across calls. You could make the argument that In my apps, I typically will only wrap a side-effecting line of a do-routine in an inline IO if it's an external function call that performs a side-effect, and that's not obvious. But in the case of assigning a property, I would either use a helper if I had one defined, or I would just do the operation directly in the do-routine. My point is: not all side effects in a do-routine have to be wrapped in an IO... so forcing a wrapping in an IO just to access the Reader env is more hostile, and I will argue it does so for no good benefit. Now, to the direct question of accessing the Reader env... do we need to make it difficult or can we reasonably offer an affordance for accessing it? Let's say I need explicit access to the const getEnv = () => IO(identity);
IO.do(function *main(){ // no env passed in here
var env = yield getEnv();
// now do whatever I want with env
}); Since I can do that, why not just pass You always have the option of NOT accessing the argument passed to the do-routine, and instead explicitly calling something like Why is accessing the function *main(env){
var { window, myBtn } = env; // this is nice!
// ..
}
function *main({ window, myBtn }){ // this is even nicer!
// ..
}
function *main(){
var { window, myBtn } = yield getEnv(); // this is workable, but isn't as nice
// ..
} The second one there is my favorite, because it declaratively exposes at the definition of the do-routine what its (Reader env) dependencies are. Besides destructuring the function *main(env){
var { document: doc } = env;
var readyCB = doIOBind(onReady,env); // doIOBind(..) is in IO-Helpers, and notice no `yield` here
doc.addEventListener("DOMContentLoaded",readyCB);
} I also often want to amend (add to, or reduce) the Reader env for a certain sub-section of do-routines (modules) in my app: function *main(env){
var btn = yield getElement("#my-btn");
var newEnv = {
...env,
btn
};
yield applyIO( IO.do(another), newEnv );
}
function *another({ document, btn }) {
// ..
} In my apps, I call the Reader env |
The thing that troubles me concerns the purpose of this library. Concerning the IO.do(function* (){
yield getElement("#my-btn").map((btn) => { btn.disable = true });
}) Obviously people are going to misuse this library, but I think that the API, and usage examples should direct people to the monadic way. Similar to the above example, I would argue that in the IO.do(function* () {
// Adding only 1 element using a `.chain`.
yield getElement("#my-button")
.chain((btn) => IO((env) => another.run({...env, btn})))
// Adding 2 elements using a `.map` and a `.chain`.
yield getElement("#my-button")
.chain((btn) => getElement("my-div").map((div) => {btn,div}))
.chain((extra) => IO((env) => another.run({...env, ...extra})))
// Like above but creating the new IO chain first, and then calling `.map` on `another.run`.
yield getElement("#my-button")
.chain((btn) => getElement("my-div").map((div) => {btn,div}))
.chain((extra) => IO((env) => ({...env, ...extra})).map(another.run) )
// Adding 2 elements using `.concat` (by turning them into arrays first).
yield getElement("#my-button").map(Array.of)
.concat((btn) => getElement("my-div").map(Array.of))
.chain(([btn,div]) => IO((env) => another.run({...env, btn, div})))
})
// No need to keep the generator function outside of an `IO.do`.
const another = IO.do(function* () {
yield IO((newEnv) => {
const {btn} = newEnv;
// Do something with btn & newEnv
})
}); The const main = IO.do(function* () {
yield getElement("#my-button")
.chain(btn => (
IO(env => btn.addEventListener("click", bind(handleClick, {...env, btn}))
);
});
const handleClick = IO.do(function* () {
yield IO(([event, {btn}]) => {
// Do something with event (and/or btn).
});
})
const bind = (io, env) => (...args) => io.run([...extra, env]) I really like the monadic idea, and I'm learning a lot (about monads) through this experience. But these kind of inconsistencies keep me awake at night. |
Think of var x = IO.do(main);
function *main() { /*.. */ } This code doesn't do anything... it merely sets up an IO monad instance IOs are only executed where you see a Guess what: in a do-routine (or any IO constructor), you can even directly access and mutate globals! Why is this OK? Because that's the point of IO monads, to offer a lazy trigger to control when side effects happen, not keep them from ever happening. Inside the do constructor, you use generator syntax instead of fluent chain'ing API syntax, because the point of the do syntax is to drop out of the declarative chain style in favor of imperative style (inside the generator). Imperative do style doesn't mean non-monadic or impure, necessarily. It just means "does not need to be expressed as a series of You can do as many nested chain calls as you like doing in there. But the point of the do syntax is to escape from the tyranny/limitations of "pure" chain calls and use imperative constructs like variable declarations/assignments, if statements, loops, or whatever else you want to do that would be awkward or hard to read in the chain API style. If you're fine doing something with "pure" chain calls, do so, and don't put your code in a do-routine. More to the bigger design question... the entire reason for Monio to exist is to provide a uniquely pragmatic flavor of the imperative do-syntax as a more welcoming "bridge" to the non-FP-centric, non-monad-zealot typical JS dev. I believe I know their mindset because I have been one for decades (only a recent partial convert to FP thinking) and I have taught literally thousands of them over the last decade. I do not believe being able to access the Reader env as an explicit object is a violation of any FP or monad principles. Can such things be abused? Of course, all JS code can be, because the engine doesn't enforce rules. And I don't think it should! That's why I choose JS (and, frankly, don't choose TS myself). My theory is: Monio can bridge the gap from non-FP, non-monadic coding to FP/monadic coding, in a way that other monad-in-JS libs have failed to do, precisely because it offers these inviting/friendly affordances. The Reader env object being directly accessible, if needed, is a critical usability affordance (among several others, like transparent Promise transformation) that makes the coding style in Monio inviting enough that someone like me is willing to convert from 25 years worth of experience in imperative JS coding to this more FP/monadic style. I find it just the right compromise between the two extremes, just enough to pull me in and get me hooked. I hope it attracts others similarly. |
Now that my TS implementation has IO support, I rewrote the build script to use try {
//...
} catch (e) {
if (v === e) {
// If we get the same error we threw, then the generator isn't catching it.
// This will cause the an "Uncaught (in promise)" runtime error though.
throw (e instanceof Error) ? e : new Error(inspect(e));
}
return next(e, "error");
} |
Concerning your response (I meant to reply in the last comment but got distracted). If I look at the const world = Just("World");
const greeting = Maybe(world);
// Helpers functions can return an IO that accepts the global environment to produce side effects.
const log = (...data) => IO(({console: _console}) => _console.log(...data));
// Our main IO.
const main = IO.do(function *main({greet}){
const msg = greeting.map(m => `${greet} ${ m }!!`);
// Uncomment this line to swap in an empty maybe
// msg = Maybe.from(null);
yield msg.fold(IO.of,log);
});
// Add custom properties to the globalThis (window / self / global / ...) when running the IO.
main.run({...globalThis, greet: "Hello"});
// => Hello World!! |
Side note: function *main(env, greet) {
const msg = greeting.map(m => `${greet} ${ m }!!`);
yield msg.fold(IO.of,log);
}
// Helpers functions can return an IO that accepts the global environment to produce side effects.
const log = (...data) => IO(({console: _console}) => _console.log(...data));
// ************************
const world = Just("World");
const greeting = Maybe(world);
IO.do( main(globalThis,"Hello") )
.run(globalThis);
// => Hello World!!
// or, using `doIO(..)` helper:
doIO( main, "Hello" )
.run(globalThis);
// => Hello World!! |
Other side note: It is better to document Concerning your side note, I wanted to show how we can add custom properties to the import axios from 'https://cdn.skypack.dev/axios';
const world = Just("Hello World");
const greeting = Maybe(world);
// Helpers functions can return an IO that accepts the global environment to produce side effects.
const log = (...data) => IO(({console: _console}) => _console.log(...data));
// Our main IO.
const main = IO.do(function *main({axios}){
const msg = greeting.map(m => `${ m }!!`);
// Uncomment this line to swap in an empty maybe
// msg = Maybe.from(null);
yield msg.fold(IO.of,log);
// Do something with axios
});
// Add custom properties to the globalThis (window / self / global / ...) when running the IO.
main.run({...globalThis, axios}); |
Whether we call it a "generator object" or an "iterator", it's definitely only an iterator that comes from a generator. There should be a sub-type of Iterator that includes what generators add. Whatever that type is, that's what
|
Yes. Same for the async versions - obviously |
You asked for more "real" examples of Monio code... I have not open-sourced the code for this app I've built yet, but I figured I could share one of the functions here: doIOBackground(setupMain).run(globalThis);
// ****************************************
function *setupMain(window = window) {
var { document, navigator, } = window;
// did we have a defined starting page?
var startPage = yield IO(() => (
(document.location.hash || "")
.toLowerCase()
.replace(/^#/,"")
.replace(/[^\w\d]+/g,"")
));
yield iif(!!startPage, $=>[
IO(() => document.location.hash = ""),
]);
// create event emitter to be shared "globally"
var events = new EventEmitter2({
wildcard: true,
maxListeners: 10,
delimiter: ":",
ignoreErrors: true,
});
// setup DOM event handling
var DOMEventManager = yield applyIO(
manageDOMEvents(events),
{
window,
document,
}
);
// setup the action-events stream
var actionEvents = yield IOEventStream(events,"action");
// normally, IO Event Streams are lazily subscribed,
// but here we want to force it to start eagerly
yield IO(() => actionEvents.start());
// get settings (from local storage)
var settings = yield applyIO(
doIO(retrieveSettings),
{ window, }
);
// initialize app management (service worker,
// PWA install, etc)
//
// NOTE: even though we yield here (to indicate
// side-effects), we aren't waiting on it; it's
// a persistent background process (IO do-routine)
yield applyIO(
doIOBackground(initAppManagement),
{
window,
document,
navigator,
events,
settings,
DOMEventManager,
}
);
// determine how to listen for changes in page visibility
var [
pageVisibilityProperty,
visibilityChangeEvent,
] = listHead(
yield listFilterInIO(
([propName,evtName]) => (
getPropIO(propName,document).map(v => v != null)
),
[
[ "visibilityState", "visibilitychange" ],
[ "webkitVisibilityState", "webkitvisibilitychange" ],
[ "mozVisibilityState", "visibilitychange" ],
]
)
) || [];
// wait for the DOM to be ready
yield whenDOMReady();
// sync the viewport size in CSS
yield applyIO(doIO(computeViewportDimensions),{ document, });
// get main DOM elements
var cardsEl = yield getElement("cards");
var behindCardEl = yield findElement(cardsEl,".behind");
var behindFrontFaceEl = yield findElement(behindCardEl,".front-face");
var behindBackFaceEl = yield findElement(behindCardEl,".back-face");
var currentCardEl = yield findElement(cardsEl,".current");
var currentFrontFaceEl = yield findElement(currentCardEl,".front-face");
var currentBackFaceEl = yield findElement(currentCardEl,".back-face");
var currentCardCoverEl = yield findElement(currentCardEl,".cover");
var nextCardEl = yield findElement(cardsEl,".next");
var nextFrontFaceEl = yield findElement(nextCardEl,".front-face");
var nextBackFaceEl = yield findElement(nextCardEl,".back-face");
// get template(s)
var tmplWelcomeCard = innerText(yield getElement("tmpl-welcome-card"));
// define a view-context to run in
var viewContext = {
window,
navigator,
document,
pageVisibilityProperty,
visibilityChangeEvent,
cardsEl,
behindCardEl,
behindFrontFaceEl,
behindBackFaceEl,
currentCardEl,
currentFrontFaceEl,
currentBackFaceEl,
currentCardCoverEl,
nextCardEl,
nextFrontFaceEl,
nextBackFaceEl,
tmplWelcomeCard,
state: {
startPage,
currentCardType: "",
currentCardFace: "front",
UIEventsAllowed: true,
optionsMenuOpen: false,
// manage hints
tapHintTimer: null,
tapHintShownTimer: null,
swipeHintTimer: null,
swipeHintShownTimer: null,
hintsStatus: {
welcomeSwipe: "hidden",
practiceSwipe: "hidden",
practiceTap: "hidden",
},
},
settings,
events,
actionEvents,
DOMEventManager,
DOMEventRouter: null,
getMainState: null,
};
// define and store view-context-bound DOM event router
var DOMEventRouter = viewContext.DOMEventRouter =
doIOBind(routeDOMEvents,viewContext);
// define state-sharing accessor
viewContext.getMainState = doIOBind(
shareStates([
"UIEventsAllowed",
"optionsMenuOpen",
]),
viewContext
);
// initialize options-menu controller
//
// NOTE: even though we yield here (to indicate
// side-effects), we aren't waiting on it; it's
// a persistent background process (IO do-routine)
yield applyIO(doIOBackground(setupOptionsMenu),{
document,
currentCardEl,
events,
DOMEventManager,
});
// run the rest in this new view-context env
return applyIO(doIO(runMain),viewContext);
} This code uses Monio as well as a companion UI framework I built on top of Monio that's called Domio. Notice that here I set up the |
That's great. |
@Eyal-Shalev Hi! I'm just resurrecting this thread to see if you were still interested in creating the d.ts files for this project. Thanks! |
I wonder if we could use something like this tool called "derive-type", to automatically generate the types, perhaps from running the instrumentation while executing the test suite. I don't think it would be perfect, but it'd probably get 99% of the types. |
Do you have any plans to convert the code to typescript?
Or add
.d.ts
files?The text was updated successfully, but these errors were encountered: