Closed brucou closed 5 years ago
Hello @brucou. Good question. Thank you for asking. I definitely see where you're coming from.
I think there are two ways to answer your question. One way is to define what purity means for generator functions and see if main
satisfies it. Another way is to consider what the use of generator functions "mean" in Turbine. I'll try to answer from both angles.
Normally when talking about the purity of functions generator functions are not part of the picture. But I think you gave a good definition of what purity should mean for a generator function.
If similarly we define equality of generator objects as generating the same sequence
I think that is very sensible. A generator function is pure if it has no side-effects and always generate the same sequence on the same input. In this sense main
is in fact pure. Even though it may not seem so.
it does not seems that two execution of main will give the same generator object. That will depend on the value entered by the user.
The main
function doesn't actually deal directly with values entered by the user. Instead it yield
s a series of components and a description of what to do with the output from these components. It constructs a description of the view (a bit like a virtual dom) and a description about how the componets should be "wired together". Both of these descriptions are simply pure data structures. The impure runComponent
then takes the description and uses it to actually create the DOM elements and set up the wiring (add event listeners, etc.).
The second way to approach the issue is to consider what Turbine does with the generator functions. I will admit that the use of generator functions is confusing. But actually, the way in which Turbine uses generator functions is quite simple and limited. We've tried to explain it in the section Understanding generator functions in the readme.
The generator function main
that you posted from the documentation is equal to the following code:
function mainWithoutGenerator() {
return span("Please enter an email address: ").chain((_) => {
return input().chain(({ inputValue: email })) => {
const isValid = email.map(isValidEmail);
return div([
"The address is ", map((b) => b ? "valid" : "invalid", isValid)
]);
});
});
}
The generator function and the yield
s is only "syntactic sugar" for nested invocations of chain
. This is a bit similar to how async/await
is syntactic sugar for calling the then
method on promises (it is also similair to do-notation is Haskell or Scala if you're familiar with that).
The generator function and the code above amounts to the same thing when plugged into runComponent
. So the generator function is pure if the code above is. And it is pure because it only calls other pure functions. For instance, seeing that chain
is pure is pretty easy. The source code is one line. It does nothing but constructs an object with two properties. You can do the same thing for the other functions as well and will find that they are all pure 😄
Does the above answer your question? If not then please keep asking! 👍 If you read the Understanding generator functions section in the readme I'd love to hear if it made sense to you. I think the use of generator functions is the most confusing part of Turbine so it's really important that we explain properly why they are used. Thus any feedback would be much appreciated 😹
Thanks for the detailed answer.
I indeed looked once to https://github.com/funkia/turbine#understanding-generator-functions, but could not get it on first reading. One of the issues that make it difficult to understand is. if I take the first example given, the lack of understandigng of the specifications of the input
function. That input
function seems to miraculously produce an inputValue
, while being called with no arguments.
I will definitely have a second look and let you know (PS : I anticipate to you that I know about monads, and I guess that input must be returning a monadic value, but a monadic value is still a value).
In the meanwhile, back to your example mainWithoutGenerator
. If we suppose input
returns a datastructure (which has to be always the same, when called with ()
, i.e. with no arguments), there is again the question of comparing this datastructure for equality. If you go with the logic down enough, you should end up in the part where there is a function which produces the input value entered by the user, and that function would not be pure (unless you define function equality as equality between source codes - only then your function would be a data structure). Haven't looked up the code yet though but that is my intuition as of now.
Will keep you updated and give feedback on the explanation for generators.
Hi again @brucou. Here is one way to understand input
that may make it easier to see how it can be pure. The following simplified pseudo-code roughly depicts how input
works.
class InputComponent {
constructor(options) {
this.options = options;
}
run(parentElement) {
const inputElm = document.createElement("input");
... use options to set attributes
parentElement.addChild(inputElm);
const inputValue = behaviorFromEvent(inputElm, "input");
return { inputValue }
}
chain(f) { ... }
}
function input(options) {
return new InputComponent(options);
}
Note that calling input
only creates a new instance of InputComponent
. Instances of InputComponent
are never mutated (in practice they are immutable) so doing that is completely pure.
The side-effects happens inside the run
method on the InputComponent
class. But this method is only ever called by the runComponent
function. Hence runComponent
is the only impure function.
Thanks for following up on this @paldepind. I gave a second to the documentation (README.md
) and in particular to the generators explanation. It is more clear now with a second lecture and the
additional information you provide in the former thread.
So:
modelView
which takes a parametrizable component factory, and a Now
thing, whatever
that is, which somehow produces the parameter for the component factoryMy problems with this documentation are the following :
go
??fgo
?? what does this do? they obviously run generators but how exactly?modelView
? Dont you only need a behaviour there?modelView(counterModel, counterView)();
: what is counterView
initial value? As { incrementClick, decrementClick }
do not exist initially (computed by counterView
), how is the initial value computed?: more magic.return [{}, {}];
: how is that the model for a list of counters?Also from the documentation, I miss :
About the semantics themselves:
thermite
. Also in flare widgets also both handle visual representation and behaviour(s).In conclusion, I found it hard to accurately evaluate the approach taken in turbine, though it is definitely interesting. I am sure a third pass at it, this time diving into the examples will help. I am giving the subject of user interface programming quite some thoughts, and while my reflexions are still in preliminary phase, you could be interested to have a look at my take on the subject. Cf. https://brucou.github.io/posts/user-interfaces-as-reactive-systems/ and later articles (all is draft stage but still reasonably advanced).
A year later, I just wanted to say that I get it now :-) And it is not even hard, just the explanation could be improved somewhat. Closing this. Thanks for the support
@brucou That's good to hear :smile:
And it is not even hard, just the explanation could be improved somewhat.
Please let us know how or just where in particular you think improvements should be made?
Referring to the first example :
and this comment
//
runComponentis the only impure function in application code;
To check that
main
is a pure function, I wanted to look at its inputs vs. outputs and possible effects it might do but I feel perplexed. How would you go about it?If I understand
main
as being a generator function which return a generator object, then it comes down to the question of how to compare two generator objects, which is the same as testing two iterators for equality, which is a similar question to comparing two functions for equality, which is seemingly undecidable in the general case (cf. https://stackoverflow.com/questions/1132051/is-finding-the-equivalence-of-two-functions-undecidable) - it is obviously an easy problem if the function's domain is finite -- that is not the case here.Zooming in on function equality, note that defining two functions as equal iff their
function.toString()
is the same, i.e. if they have the same source code, does not lead to interesting properties. The general understanding of function equality is that f === g if for every input x of f's domain, f(x)=g(x). There I am talkign about function who do not perform effects, if they do, the definition could be extended to include the fact that they perform the same effects (and that extends the problem to defining equality on effects...).If similarly we define equality of generator objects as generating the same sequence, then it does not seems that two execution of main will give the same generator object. That will depend on the value entered by the user.
Where did I go wrong?