Coder Social home page Coder Social logo

funkia / turbine Goto Github PK

View Code? Open in Web Editor NEW
684.0 26.0 27.0 4.61 MB

Purely functional frontend framework for building web applications

License: MIT License

TypeScript 99.65% JavaScript 0.35%
typescript functional-reactive-programming framework pure javascript frp

turbine's Introduction

Turbine

A purely functional frontend framework based on functional reactive programming. Experimental.

Gitter Build Status codecov

Table of contents

Why Turbine?

The JavaScript world is full of frameworks. So why another one? Because we want something different. We want something that is purely functional without compromises. Something that takes the best lessons from existing JavaScript frameworks and couples them with the powerful techniques found in functional languages like Haskell. We want a framework that is highly expressive. Because when functional programming is at its best it gives you more power, not less. Turbine is supposed to be approachable for typical JavaScript developers while still preserving the benefits that comes from embracing purely functional programming.

We have done our best to realize our goal. But we are not done yet. We hope you will find Turbine interesting, try it and maybe even help us making it even better.

Examples

Email validator

See the example live and editable here.

const isValidEmail = (s: string) => /.+@.+\..+/i.test(s);

const app = component((on) => {
  const isValid = on.email.map(isValidEmail);
  return [
    span("Please enter an email address: "),
    input().use({ email: "value" }),
    div(["The address is ", isValid.map((b) => (b ? "valid" : "invalid"))])
  ];
});

// `runComponent` is the only impure function in application code
runComponent("#mount", main);

Counter

See the example live and editable here.

const counter = component((on, start) => {
  const count = start(accum((n, m) => n + m, 0, on.delta));
  return E.div([
    "Counter ",
    count,
    E.button({ class: "btn btn-default" }, " + ").use(o => ({
      delta: o.click.mapTo(1)
    })),
    E.button({ class: "btn btn-default" }, " - ").use(o => ({
      delta: o.click.mapTo(-1)
    }))
  ]);
});

// `runComponent` is the only impure function in application code
runComponent("#mount", counter);

See more examples here.

High-level overview

Here our some of our key features.

  • Purely functional. A Turbine app is made up of only pure functions.
  • Leverage TypeScript and runtime checking to improve the developing experience.
  • Based on classic FRP. Behaviors represents values that change over time and streams provide reactivity. Turbine uses the FRP library Hareactive.
  • A component-based architecture. Components are immutable, encapsulated and composable. Components are monads and are typically used and composed with do-notation (we implement do-notation with generators).
  • Constructed DOM elements reacts directly to behaviors and streams. This avoids the overhead of using virtual DOM and should lead to great performance.
  • Side-effects are expressed with a declarative IO monad. This allows for easy testing of code with side-effects. Furthermore, the IO-monad is integrated with FRP.
  • The entire data flow through applications is explicit and easy to follow.
  • Our libraries are available both as CommonJS and ES2015 modules. This allows for tree-shaking.

Here are some of the features we want to implement and goals we're working towards.

  • Declarative and concise testing of time-dependent FRP code.
  • Performance. We think Turbine can be made very efficient. But we are not yet at a point where we focus on performance.
  • Support for server side rendering.
  • Browser devtools for easier development and debugging.
  • Hot-module replacement (if possible given our design).

Principles

This section describes some of the key principles and ideas underlying the design of Turbine.

Purely functional

Turbine is purely functional. We mean that in the most strict sense of the term. In a Turbine app, every single expression is pure. This gives a huge benefit in how easy it is to understand and maintain a Turbine app is.

One benefit of the complete purity is that every function in Turbine supports what is called "referential transparency". This means that an expression can always be replaced with its value.

As a simple example, say you have the following code:

const view = div([
  myComponent({ foo: "bar", something: 12 }),
  myComponent({ foo: "bar", something: 12 })
]);

One may notice that myComponent is called twice with the exact same arguments. Since all functions in a Turbine app are pure myComponent is no exception. Hence, we can make the following simple refactoring.

const component = myComponent({foo: "bar", something: 12}),
const view = div([
  component,
  component
]);

Such refactorings can always be safely done in Turbine.

Completely explicit data flow

One significant challenge when writing an interactive frontend application is how to manage the data flow through an application.

In Turbine we have strived to create an architecture where the data flow is easy to follow and understand. For us, this means that when looking at any piece of code it should be possible to see what other parts of the application it affects and what other parts it is affected by.

One manifestation of this principle is that in Turbine it is very simple to see how the model affects the view and how the view affects the model. The figure below illustrates this.

modelView figure

The arrows represent data flow between the model and the view. Note how these "conceptual arrows" are clearly expressed in the code. For instance, by looking at the buttons we can see exactly what output they produce.

Declarative models

Imperative programming is about doing. Functional programming is about being. This mean that ideally a functional program should be about defining what things are. That property is what makes functional programs declarative.

Below is a model from the counters example. Notice how the model consists of nothing but a series of const statements.

function* counterModel({ incrementClick, decrementClick, deleteClick }) {
  const increment = incrementClick.mapTo(1);
  const decrement = decrementClick.mapTo(-1);
  const deleteS = deleteClick.mapTo(id);
  const count = yield accum(add, 0, combine(increment, decrement));
  return { count, deleteS };
}

Each line is a declaration of a piece of the state. All models in Turbine follows this pattern. This makes state in a Turbine app very easy to understand. One can look at a single definition and be certain that it tells everything there is to know about that specific piece of state.

This is in sharp contrast to frameworks that mutate state or frameworks where state is stepped forward by reducer functions. With such approaches a single piece of state can potentially be affected and changed in several places. That can make it hard to understand how the state evolves. The benefits of having a definition as a single source of truth is lost.

Installation

npm install @funkia/turbine @funkia/hareactive

Hareactive is a peer dependency. It is the FRP library that that Turbine is based upon.

Alternatively, for quickly trying out Turbine you may want to see our Turbine starter kit.

More examples

Here is a series of examples that demonstrate how to use Turbine. Approximately listed in order of increasing complexity.

  • Email validator — Very simple example of an email validator.
  • Fahrenheit celsius — A converter between fahrenheit and celsius.
  • Zip codes — A zip code validator. Shows one way of doing HTTP-requests with the IO-monad.
  • Continuous time — Shows how to utilize continuous time.
  • Counters — A list of counters. Demonstrates nested components, managing a list of components and how child components can communicate with parent components.
  • Todo — An implementation of the classic TodoMVC application.

Tutorial

In this tutorial, we will build a simple application with a list of counters. The application will be simple but not completely trivial. Along the way, most of the key concepts in Turbine will be explained. We will see how to create HTML, how to create custom components, how a component can be nested and how it can share state with its parent.

Please open an issue if you have questions regarding the tutorial or ideas for improvements.

The final result and the intermediate states can be seen by cloning this git repository, going into the directory with the counters example and running webpack to serve the application.

git clone https://github.com/funkia/turbine/
cd turbine/examples/counters
npm run start

FRP

Turbine builds on top of the FRP library Hareactive. The two key concepts from FRP are behavior and stream. They are documented in more detail in the Hareactive readme. But the most important things to understand are behavior and stream.

  • Behavior represents values that change over time. For instance, the position of the mouse or the number of times a user has clicked a button.
  • Stream represents discrete events that happen over time. For instance click events.

What is Component

On top of the FRP primitives Turbine adds Component. Component is the key concept in Turbine. Once you understand Component—and how to use it—you understand Turbine. A Turbine app is just one big component.

Here is a high-level overview of what a component is.

  • Components can contain logic expressed through operations on behaviors and streams.
  • Components are encapsulated and have completely private state.
  • Components contain output through which they selectively decide what state they share with their parent.
  • Components write DOM elements as children to their parent. They can write zero, one or more DOM elements.
  • Components can declare side-effects expressed as IO-actions.
  • Components are composable—one component can be combined with another component and the result is a third component.

A Component in Turbine is pure and immutable. A Component can be thought of as a huge description of all of the above mentioned things. For instance, a Component contains a description about what its DOM look like. That part is a bit like virtual DOM. But, on top op that the description also explain how the DOM changes over time. The description also tells what output the Component contains. More on that later.

Creating HTML-elements

Turbine includes functions for creating components that represent standard HTML-elements. When you create your own components they will be made of these.

The element functions accept two arguments, both of which are optional. The first is an object describing various things like attributes, classes, etc. The second argument is a child component. For instance, to create a div with a span child we would write.

const myDiv = div({ class: "foo" }, span("Some text"));

The element functions are overloaded. So instead of giving span a component as child we can give it a string. The element functions also accept an array of child elements like this.

const myDiv = div({ class: "foo" }, [h1("A header"), p("Some text")]);

Using this we can build arbitrarily complex HTML. As an example we will build a simple view for a counter in our counter-application.

import { elements, runComponent } from "@funkia/turbine";
const { br, div, button } = elements;

// Counter
const counterView = div(["Counter ", 1, " ", button("+"), " ", button("-")]);

runComponent("body", counterView);

We define counterView as div-element with some text and two buttons inside. Since div returns a component counterView is a component. And a Turbine application is just a component so we have a complete application. We run the application on the last line when we call runComponent. It is an impure function that takes a selector, a component and runs the component with the found element as parent. You can view the entire code in version1.ts.

Dynamic HTML

The counterView above is completely static. The buttons do nothing and we hard-coded the value 1 into the view. Our next task is to make the program interactive.

Anywhere where we can give the element functions a constant value of a certain type we can alternatively give them a behavior with a value of that type. For instance, if we have a string-valued behavior we can use it like this

const mySpan = span(stringBehavior);

This will construct a component representing a span element with text content that is kept up to date with the value of the behavior.

To make the count in our counter view dynamic we turn it into a function that takes a behavior of a number and inserts it into the view.

const counterView = ({ count }: CounterViewInput) =>
  div(["Counter ", count, " ", button("+"), " ", button("-")]);

Because it will be easier going forward counterView takes an object with a count property.

Output from components

The above covers the input to the counter view. We now need to get output from it.

Remember that we mentioned how a Turbine component is a description about what the component will behave and look like. Part of that description also explains what output will come from the component.

To get a feel for what "output" means it may be helpful to mention a few examples.

  • A button outputs, among other things, a stream of click events. So part of its output is a stream of the type Stream<ClickEvent>>.
  • An input box's output includes a behavior of the text inside the input. The type would be Behavior<string>.
  • A checkbox might output a behavior representing whether it is checked or not. It would have type Behavior<boolean>.

One way of looking at the output is that it is the information we would like to get from the view.

In practice a component will almost always output more than a single stream or behavior. By convention the output is therefore almost alway an object.

Components are represented by a generic type Component<O, A>. The A represents the available output of the component and the O represents the selected out of the component. The difference between selected and available output is highlighted in the example below.

Constructing an input element looks like this

const usernameInput = input({ placeholder: "Username" });

The type of the component constructed above is as follows ( the ... refer to the fact that we have omitted a lot of the output to keep things simple).

Component<{}, { value: Behavior<string>, click: Stream<ClickEvent>, ... }>

Among its available output an input element produces a string valued behavior named value that contains the current content of the input element.

Like this input component a newly constructed component always have {} as its selected output. This means that initially no output is selected. We can move output from the available output into the selected output by using the output method on components.

const usernameInput = input({
  attrs: { placeholder: "Username" }
}).output({ username: "value" });

Here usernameInput has the type

Component<{ username: Behavior<string> }, ...>

In the above code the invocation to output means: from the object of available output take the value property and add it to the object of selected output with the property name username.

The difference between available output and selected output matters when components are combined. In most cases, when components are composed or combined all their available output is discarded and only the selected output becomes part of the combined component.

For instance, in the code below the div is given two children.

div([
  button("Click me").output({ firstButtonClick: "click" }),
  button("Don't click me")
]);

The div element composes the two buttons. When doing so all output from the buttons except for the click stream from the first button is discarded.

Using the output method is a bit like adding event handlers in other UI frameworks. There are many events that one can add handlers to but on any given element only a few events are actually of interest and for these one will add event handlers. Similarly, in Turbine components have a lot of available output but only the piece of it that gets selected will be output in the end.

Back to the counters app. We want our counter view to produce two streams as output. One stream should be from whenever the first button is clicked and the other stream should contain clicks from the second button. That is, the view's output should have the type

{
  incrementClick: Stream<ClickEvent>,
  decrementClick: Stream<ClickEvent>
}

We can achieve that by using the output method in each button.

const counterView = ({ count }) =>
  div([
    "Counter ",
    count,
    " ",
    button("+").output({ incrementClick: "click" }),
    " ",
    button("-").output({ decrementClick: "click" })
  ]);

The call to output on each button tells them what output we are interested in. The first buttons selected output is then object with a stream named incrementClick and the later and object with one named decrementClick.

The div function then combines the selected output from the components in the array passed to it and output that as its own selected output. The result is that counterView returns a component that produces two streams as its output.

An analogy with promises

As mentioned above using the output method is a bit like adding event listeners in other frameworks. However, there are fundamental differences between the two things. If you are familiar with how asynchronous functions that takes callbacks differ from asynchronous function that returns promises then the following analogy may help understand this difference.

An asynchronous function for reading a file may look like this

readFileCallback("foo.txt", (file) => ...)

A similar function based on promises looks like this.

readFilePromise("foo.txt").then((file) => ...)

Notice that the readFileCallback function does not return the file that it reads. The file is instead passed to a callback that it gets as an argument. The readFilePromise function on the other hand returns the file wrapped in a promise of the type Promise<File>.

Most UI frameworks are similar to the readFileCallback function. In order to know when a button is pressed you do something like this.

<button onClick={(clickEvent) => ...}>Click me</button>

The click events on the button are not returned from the button function. Instead they are passed to a callback (or event handler) that the button function gets as an argumen.

The same thing in Turbine looks like this.

button("Click me").output({ click: "click" });

This is similar to the readFilePromise function. The button function does not take any callbacks but returns a stream of clicks wrapped in a component of the type Component<{ click: Stream<ClickEvent> }, ...>.

This example should give some intuition about how Turbine differs from most other frameworks. Other frameworks handle events similar to doing asynchronous computations with callbacks but Turbine handle events similarly to doing asynchronous computations with promises. In particular when creating components the output is returned as part of the component.

Adding a model

We now need to add a model with some logic to our counter view. The model needs to handle the increment and decrement stream and turn them into a behavior that represents the current count.

Turbine offers the function modelView for creating components with logic. modelView takes two arguments. The first describes the logic and the second the view. This keeps the logic neatly separated from the view.

The second argument to modelView, the view, is a function that returns a component. We already have such a function: counterView.

The first argument is a function that returns a Now-computation. You don't have to fully understand Now. One of the things it does is to make it possible to create stateful behaviors. The model function will as input receive the output from the component that the view function returns. The result of the Now-computation will be passed on to the view function and will be the output of the component that modelView returns. Here is how we use to create our counter component.

function* counterModel({ incrementClick, decrementClick }: CounterModelInput) {
  const increment = incrementClick.mapTo(1);
  const decrement = decrementClick.mapTo(-1);
  const changes = combine(increment, decrement);
  const count = yield accum((n, m) => n + m, 0, changes);
  return { count };
}

const counter = modelView(counterModel, counterView)();

Note that there is a cyclic dependency between the model and the view. The figure below illustrates this.

modelView figure

We now have a fully functional counter. You have now seen how to create a simple component with encapsulated state and logic. The current code can be seen in version2.ts.

Creating a list of counters

Our next step is to create a list of counters. To do that we will create a new component called counterList. The component will contain a list of counter components as well as a button for adding counters to the list.

Let's begin by defining a view function that creates a header and a button.

function* counterListView() {
  yield h1("Counters");
  const { click: addCounter } = yield button(
    { class: "btn btn-primary" },
    "Add counter"
  );
  return { addCounter };
}

We hook the view up to a model using modelView. Again, the model function receives the return value from the view function.

const counterList = modelView(counterListModel, counterListView);

const counterListModel = fgo(function*({ addCounter, listOut }) {
  const nextId = yield scan(add, 2, addCounter.mapTo(1));
  const appendCounterFn = map(
    (id) => (ids: number[]) => ids.concat([id]),
    nextId
  );
  const counterIds = yield accum(apply, [0], appendCounterFn);
  return { counterIds };
});

const counterListView = ({ sum, counterIds }) => [
  h1("Counters"),
  button({ class: "btn btn-primary" }, "Add counter").output({
    addCounter: "click"
  }),
  ul(list(counter, counterIds).output((o) => ({ listOut: o })))
];

const counterList = modelView(counterListModel, counterListView);

To create a dynamic list of counters we have to use the list function.

Documentation

Understanding generator functions

Turbine's use of generator functions may seem a bit puzzling at first. For instance, it may seem like generator functions serve two different purposes. One when they're used in the model and another when they're used in the view

But, what they do under the hood is exactly the same in both cases. The key to understand is that generator functions is just sugar for calling chain several times in succession.

When we use chain on components we can combine elements and pipe output from one component into the next. The code below combines two input elements with a span element that shows the concatenation of the text in the two input fields.

input({ attrs: { placeholder: "foo" } }).chain(({ value: aValue }) =>
  input().chain(({ value: bValue }) => {
    const concatenated = lift((a, b) => a + b, aValue, bValue);
    return span(["Concatenated text: ", concatenated]).mapTo({ concatenated });
  })
);

However, the above code is very awkward as each invocation of chain adds an extra layer of nesting. To solve this problem we use generators.

go(function*() {
  const { value: aValue } = yield input();
  const { value: bValue } = yield input();
  const concatenated = lift((a, b) => a + b, aValue, bValue);
  yield span(["Concatenated text: ", concatenated]);
  return { concatenated };
});

The above code does exactly the same as the previous example. But it is a lot easier to read!

The go function works like this. We yield a value with a chain method. go then calls chain on the yielded value. go calls chain with a function that continues the generator function with the value that chain passes it. The end result is a value of the same type that we yield inside the generator function. When we yield a Component<A> we will get an A back inside the generator function.

Finally we return a value and that value will be the output of the component that go returns.

Here is another example. The following code uses chain explicitly.

const view = button("Accept").chain(({ click: acceptClick }) =>
  button("Reject").map(({ click: rejectClick }) => ({
    acceptClick,
    rejectClick
  }))
);

The above code is equivalent to the following.

const view = go(function*() {
  const { click: acceptClick } = yield button("Accept");
  const { click: rejectClick } = yield button("Reject");
  return { acceptClick, rejectClick };
});

Again, the code that uses generator functions is a lot easier to read. This is why they're useful in Turbine.

Component is not the only type in Turbine that has a chain method. Now and Behavior does as well. And since go is only sugar for calling chain it works with these types as well.

API

The API documentation is incomplete. See also the examples, the tutorial, the Hareactive documentation and this tutorial about IO.

Component

Component#map

Mapping over a component is a way of applying a function to the output of a component. If a component has output of type A then we can map a function from A to B over the component and get a new component whose output is of type B.

In the example below input creates a component with an object as output. The object contains a behavior named value. The function given to map receives the output from the component.

We then call map on the behavior value and take the length of the string. The result is that usernameInput has the type Component<Behavior<number>> because it's mapped output is a number-valued behavior whose value is the current length of the text in the input element.

const usernameInput = input({ class: "form-control" }).map((output) =>
  output.value.map((s) => s.length)
);

Component#chain

map makes it possible to transform and change the output from a component. However, it does not make it possible to take output from one component and pipe it into another component. That is where chain enters the picture. The type of the chain method is as follows.

chain((output: Output) => Component<NewOutput>): Component<NewOutput>;

The chain method on a components with output Output takes a function that takes Output as argument and returns a new component. Here is an example. An invocation component.chain(fn) returns a new component that works like this:

  • The output from component is passed to fn.
  • fn returns a new component, let's call it component2
  • The DOM-elements from component and component2 are both added to the parent.
  • The output is the output from component2.

Here is an example.

input().chain((inputOutput) => span(inputOutput.value));

The above example boils down to this:

Create input component   Create span component with text content
                               
input().chain((inputOutput) => span(inputOutput.value));
                                                 
      Output from input-element       Behavior of text in input-element

The result is an input element followed by a span element. When something is written in the input the text in the span element is updated accordingly.

loop

Sometimes situations arise where there is a cyclic dependency between two components.

For instance, you may have a function that creates a component that shows the value of an input string-value behavior and outputs a string-valued behavior.

const myComponent = (b: Behavior<string>) => span(b).chain((_) => input());

Now we'd have a cyclic dependency if we wanted to construct two of these views so that the first showed the output from the second and the second showed output from the first. With loop we can do it like this:

loop(({ output1, output2 }) =>
  go(function*() {
    const output1_ = yield myComponent(output2);
    const output2_ = yield myComponent(output1);
    return { output1: output1_, output2: output2_ };
  })
);

The loop functional seems pretty magical. It has the following signature (slightly simplified):

loop<A extends ReactiveObject>(f: (a: A) => Component<A>): Component<A>

I.e. loop takes a function that returns a component whose output has the same type as the argument to the function. loop then passes the output in as argument to the function. That is, f will as argument receive the output from the component it returns. The only restriction is that the output from the component must be an object with streams and/or behaviors as values.

Visually it looks like this.

loop figure

modelView

The modelView functions makes it possible to create components where the view is decoupled from the model and its logic.

modelView takes two arguments:

  • The model which is a function that returns a Now computation. The Now computation is run when the component is being created.
  • The view which is a function that returns a Component.

modelView establishes a circular dependency between the model and the view. The model returns a Now computation and the result of this computation is passed into the view function. The view function then returns a component. The output of the component is passed to the model function.

Visually the circular dependency looks like this.

modelView figure

modelView returns a function that returns a component. The arguments given to this function will be passed along to both the model and the view functions. This makes it easy to create components that take input.

const myComponent = modelView(
  (outputFromView, arg1, arg2) => ...,
  (outputFromModel, arg1, arg2) => ...
);

myComponent("foo", "bar");

list

The list function is used to create dynamic lists in the UI.

Note: If you are familiar with frameworks like Angular or Vue then you can think of list as being similar to ngRepeat in Angular 1, ngFor in Angular 2, and v-for in Vue.

The list function has the following type.

function list<A, O>(
  componentCreator: (a: A) => Component<O, any>,
  listB: Behavior<A[]>,
  getKey: (a: A, index: number) => number | string = id
): Component<{}, Behavior<O[]>>;

The first parameter, componentCreator, is a function that takes a value of type A and returns a component. This function will be invoked to create the elements of the dynamic list. The second argument, listB, is a behavior of an array where the elements in the array are of some type A.

The list function will return a component that at any given point is time is equivalent to applying componentCreator to the current array in listB and then showing the resulting components one after another.

Whenever listB changes the component returned by list will react to those changes and keep the displayed list up-to-date. To do this, the last argument, the getKey function, is used to figure out how elements are moved, removed, or added. Therefore getKey should return a value that is unique for each element.

The following example illustrates the above. Let us say we have a list of users where each user is an object with an id and a username:

type User = {
  id: number;
  username: string;
};

The current list of users is represented by a behavior users: Behavior<User[]>. We want to display the users in a list with their username being editable. This can be achieved with the list function.

list((user) => input({ value: user.username }), users, (user) => user.id);

If the users behavior starts out with the value

[{ username: "foo", id: 1 }, { username: "bar", id: 2 }];

Then the component created by calling list will produce HTML like this

<input value="foo" /> <input value="bar" />

Now, if the value of users changes into

[
  { username: "baz", id: 3}
  { username: "bar", id: 2 }
  { username: "foo", id: 1 },
]

Then list will reorder the two existing input elements and insert a new input element in the beginning. Thanks to the getKey function list can efficiently do this by applying getKey to the old and the current value of the list and figure out how the elements have moved around.

SVG

You can use embed SVG in Turbine in much the same way you'd embed it in HTML:

svg({ height: "100", width: "100" }, [
  circle({
    cx: "50",
    cy: "50",
    r: "40",
    fill: "red"
  }),
  svgText({ x: 100, y: 30 }, "Hello SVG!")
]);

The only element with a different name is svgText because text in Turbine is an HTML Text Node.

Contributing

Turbine is developed by Funkia. We write functional libraries. You can be a part of it too. Share your feedback and ideas. We also love PRs.

Run tests once with the below command. It will additionally generate an HTML coverage report in ./coverage.

npm test

Continuously run the tests with

npm run test-watch

turbine's People

Contributors

deklanw avatar dependabot[bot] avatar dmitriz avatar fbn avatar j-mueller avatar jkzing avatar jomik avatar limemloh avatar paldepind avatar stevekrouse avatar vrobinson avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

turbine's Issues

Constructors in the API documentation

I find constructors very useful, particularly in creating mock code or to do sanity checks to test what's wrong, yet the only way I found them is by going into the test/ of each project. I would love these to be in the documentation for each project:

Behavior.of
Future.of
IO.of

(Let me know if you'd like me to tackle this one.)

Fancy element creator

This issue is for writing down ideas related to a fancy way of creating elements based on tagged template literals.

Basic selector for adding classes and id.

li`#my-btn.btn.btn-default`(child)

Setting a class and a property with a string valued behavior.

input`.foo.${beh}.bar[value=${valB}]`(child)

Switching between classes with a boolean valued behavior and a syntax similair to JavaScript's a ? b : c.

li`.btn.${beh}?btn-default:btn-warning`(child)

The :-part is optional. So if only a single class is to be toggled on and off the following syntax can be used.

li`.btn.${beh}?btn-default`(child)

Just some ideas 😄

Polishing up the explanation

I have been looking through the introduction to the output and it is still looks less clear than I'd like it to be. It is really a unique thing of this library and can easily scare away people if not made as simple as possible, or even simpler :)

Unfortunately, React didn't help making its components less confusing. There are component classes or constructors and there are instances. That look exactly the same in JSX, but who uses JSX anyway? :)

We don't have the JSX problem here, but it might be still more clear to emphasise the difference:

const componentInstance = myComponent({foo: "bar", something: 12}),
const view = div([
  componentInstance,
  componentInstance,
]);

Each line is a declaration of a piece of the state.

Not sure I understand this one ;)
The state is a vague concept - is any variable part of the state?
Or only the arguments of the view?

HTML-elements

Are they special component instances? Like in React?

The above covers the input to the counter view. We now need to get output from it. All components in Turbine can produce output. Components are represented by a generic type Component. The A represents the output of the component.

I would like to demystify "produce output" here.
So the component instance is wrapping its output.
Is it possible to extract it directly? Or is it impure?

The output object given to the button functions tells them what output to produce.

button({ output: { incrementClick: "click" } }, "+"),

So does it mean that only this output is produced?
So the whole output object is stored as { incrementClick: clickStream }?

  const {inputValue: email} = yield input();

This one is also a bit tricky, would it be possible to break down?
What does the yield do here?
E.g. in comparison with something like that:

  const {inputValue: email} = yield {inputValue: `[email protected]`}

So these are some questions a reader might come up with,
I've thought to put them for the record. :)

not assignable to type errors

$ npm run counters

> [email protected] counters /Users/zaitsev/Repos/turbine/examples
> webpack-dev-server --content-base counters counters/src/index.ts

Project is running at http://localhost:8080/
webpack output is served from /
Content not from webpack is served from /Users/zaitsev/Repos/turbine/examples/counters
ts-loader: Using [email protected] and /Users/zaitsev/Repos/turbine/examples/counters/tsconfig.json
webpack: wait until bundle finished: /
Hash: 68ba1eafae3e545fd4ee
Version: webpack 2.2.1
Time: 9731ms
    Asset    Size  Chunks                    Chunk Names
bundle.js  681 kB       0  [emitted]  [big]  main
chunk    {0} bundle.js (main) 556 kB [entry] [rendered]
   [12] ./~/@funkia/jabz/dist/es/index.js 389 bytes {0} [built]
   [20] ../src/index.ts 401 bytes {0} [built]
   [29] ./~/@funkia/hareactive/index.js 569 bytes {0} [built]
   [88] ./counters/src/index.ts 1.64 kB {0} [built] [3 errors]
   [89] (webpack)-dev-server/client?http://localhost:8080 5.28 kB {0} [built]
  [102] ./counters/src/version1.ts 340 bytes {0} [built]
  [103] ./counters/src/version2.ts 905 bytes {0} [built] [1 error]
  [104] ./counters/src/version3.ts 1.66 kB {0} [built] [3 errors]
  [105] ./counters/src/version4.ts 2.26 kB {0} [built] [5 errors]
  [145] ./~/strip-ansi/index.js 161 bytes {0} [built]
  [146] ./~/url/url.js 23.3 kB {0} [built]
  [148] (webpack)-dev-server/client/overlay.js 3.6 kB {0} [built]
  [149] (webpack)-dev-server/client/socket.js 856 bytes {0} [built]
  [151] (webpack)/hot/emitter.js 77 bytes {0} [built]
  [171] multi (webpack)-dev-server/client?http://localhost:8080 ./counters/src/index.ts 40 bytes {0} [built]
     + 157 hidden modules

ERROR in ./counters/src/version4.ts
(43,3): error TS2322: Type 'Component<{} & OutputStream<{}> & BehaviorOutput<{}> & DefaultOutput>' is not assignable to type 'Component<CounterModelInput>'.
  Type '{} & OutputStream<{}> & BehaviorOutput<{}> & DefaultOutput' is not assignable to type 'CounterModelInput'.
    Property 'incrementClick' is missing in type '{} & OutputStream<{}> & BehaviorOutput<{}> & DefaultOutput'.

ERROR in ./counters/src/version4.ts
(55,41): error TS2345: Argument of type '({count}: CounterModelOut) => Component<CounterModelInput>' is not assignable to parameter of type 'View1<ReactivesObject, CounterModelInput, number>'.
  Type '({count}: CounterModelOut) => Component<CounterModelInput>' is not assignable to type '(m: ReactivesObject, a: number) => Iterator<Component<any>>'.
    Types of parameters '__0' and 'm' are incompatible.
      Type 'ReactivesObject' is not assignable to type 'CounterModelOut'.
        Property 'count' is missing in type 'ReactivesObject'.

ERROR in ./counters/src/version4.ts
(68,40): error TS2346: Supplied parameters do not match any signature of call target.

ERROR in ./counters/src/version4.ts
(90,58): error TS2345: Argument of type 'Behavior<number[]>' is not assignable to parameter of type 'Behavior<number[]>'.

ERROR in ./counters/src/version4.ts
(94,38): error TS2344: Type 'ToView' does not satisfy the constraint 'ReactivesObject'.
  Property 'counterIds' is incompatible with index signature.
    Type 'Behavior<number[]>' is not assignable to type 'Behavior<any> | Stream<any>'.
      Type 'Behavior<number[]>' is not assignable to type 'Stream<any>'.
        Property 'combine' is missing in type 'Behavior<number[]>'.

ERROR in ./counters/src/version3.ts
(42,17): error TS2453: The type argument for type parameter 'V' cannot be inferred from the usage. Consider specifying the type arguments explicitly.
  Type argument candidate 'CounterModelInput' is not a valid type argument because it is not a supertype of candidate '{} & OutputStream<{}> & BehaviorOutput<{}> & DefaultOutput'.
    Property 'incrementClick' is missing in type '{} & OutputStream<{}> & BehaviorOutput<{}> & DefaultOutput'.

ERROR in ./counters/src/version3.ts
(66,44): error TS2345: Argument of type 'Behavior<number[]>' is not assignable to parameter of type 'Behavior<number[]>'.
  Types of property 'child' are incompatible.
    Type 'Observer<any>' is not assignable to type 'Observer<any>'. Two different types with this name exist, but they are unrelated.

ERROR in ./counters/src/version3.ts
(70,49): error TS2345: Argument of type '({sum, counterIds}: ViewInput) => Iterator<Component<any>>' is not assignable to parameter of type 'View1<ReactivesObject, ModelInput, {}>'.
  Type '({sum, counterIds}: ViewInput) => Iterator<Component<any>>' is not assignable to type '(m: ReactivesObject, a: {}) => Iterator<Component<any>>'.
    Types of parameters '__0' and 'm' are incompatible.
      Type 'ReactivesObject' is not assignable to type 'ViewInput'.
        Property 'counterIds' is missing in type 'ReactivesObject'.

ERROR in ./counters/src/version2.ts
(34,17): error TS2453: The type argument for type parameter 'V' cannot be inferred from the usage. Consider specifying the type arguments explicitly.
  Type argument candidate 'CounterModelInput' is not a valid type argument because it is not a supertype of candidate '{} & OutputStream<{}> & BehaviorOutput<{}> & DefaultOutput'.
    Property 'incrementClick' is missing in type '{} & OutputStream<{}> & BehaviorOutput<{}> & DefaultOutput'.

ERROR in ./counters/src/index.ts
(25,17): error TS2345: Argument of type '{ class: string; classToggle: { active: Behavior<boolean>; }; }' is not assignable to parameter of type 'InitialProperties'.
  Types of property 'classToggle' are incompatible.
    Type '{ active: Behavior<boolean>; }' is not assignable to type '{ [name: string]: boolean | Behavior<boolean>; }'.
      Property 'active' is incompatible with index signature.
        Type 'Behavior<boolean>' is not assignable to type 'boolean | Behavior<boolean>'.
          Type 'Behavior<boolean>' is not assignable to type 'Behavior<boolean>'. Two different types with this name exist, but they are unrelated.
            Types of property 'child' are incompatible.
              Type 'Observer<any>' is not assignable to type 'Observer<any>'. Two different types with this name exist, but they are unrelated.
                Types of property 'changeStateDown' are incompatible.
                  Type '(state: State) => void' is not assignable to type '(state: State) => void'. Two different types with this name exist, but they are unrelated.
                    Types of parameters 'state' and 'state' are incompatible.
                      Type 'State' is not assignable to type 'State'. Two different types with this name exist, but they are unrelated.

ERROR in ./counters/src/index.ts
(39,35): error TS2344: Type 'FromModel' does not satisfy the constraint 'ReactivesObject'.
  Property 'selected' is incompatible with index signature.
    Type 'Behavior<"1" | "2" | "3" | "4">' is not assignable to type 'Behavior<any> | Stream<any>'.
      Type 'Behavior<"1" | "2" | "3" | "4">' is not assignable to type 'Stream<any>'.
        Property 'combine' is missing in type 'Behavior<"1" | "2" | "3" | "4">'.

ERROR in ./counters/src/index.ts
(51,31): error TS2346: Supplied parameters do not match any signature of call target.
webpack: Failed to compile.

not an issue - question about purity of main

Referring to the first example :

const isValidEmail = (s: string) => s.match(/.+@.+\..+/i);

function* main() {
  yield span("Please enter an email address: ");
  const { inputValue: email } = yield input();
  const isValid = email.map(isValidEmail);
  yield div([
    "The address is ", map((b) => b ? "valid" : "invalid", isValid)
  ]);
}

// `runComponent` is the only impure function in application code
runComponent("#mount", main);

and this comment // runComponent is 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?

Single-page documentation

Documentation in a single page, such as lodash, is amazing.

Part of the appeal of Turbine is that it's built modularly on top of Harereactive, IO, and jabz, but it's super annoying to need 3-4 documentation pages open and have to flip between them, ctl-f-ing on each page. It'd be much better if we could ctl-f on a single page for what we need.

However, I wouldn't want to create an extra place you need to maintain documentation for all 4 projects. I wonder if there's some way to write documentation in each project and the automatically pull the documentation for each version into a centralized tool... I wonder what other projects with this problem do

Simple server side rendering

With a way to stringify a component it would be possible to support simple server side rendering.

The API could consist of componentToString and serveComponent. The first would turn a component into a string and the second would return a NodeJS compatible endpoint function.

Example.

app.get("/app", serveComponent("index.html", "body", myComponent));

This would offer a really simple way of getting many of the benefits from server-side rendering with very little effort from end-users.

filterApply undefined behavior bug

filterApply(Behavior.of(undefined).map(() => () => true), stream).log()

Upon an event from stream errors: predicate.at(...) is not a function.

If you replaced undefined with 1, the error goes away.

Hide `go`

Hide the go function so that generator functions can optionally be supplied directly.

Versioned documentation

I wasted a lot of time today trying to use input because that's what it is in the current README on Github, but that's a brand new change, so the version I was using had it calledinputValue.

Is the proper way to read Turbine documentation to go to the README on the commit hash of the released version you're using? Or would you recommend always trying to use code at the master hash and stay as up to date as possible? Either way, let's put a warning about this somewhere in the documentation for people.

Potentially the solution is a documentation page separate from Github that allows you to select your version, but that requires work...

And if I'm being honest, I don't think that you're "fully done" making a change to the library until all example throughout the codebase (including on codesandbox.io) reflect the new style. I don't think it's kosher to leave past examples up.

What would mitigate leaving past examples up is is if the version number (or commit hash) would be in import definitions at the top of the file, instead of in the package.json, such as import { elements, runComponent } from "@funkia/[email protected]"; However, I imagine that's not something you can change...

idea: JSX to Turbine

It'd be neat if we could write JSX in the Turbine views. It'd require a custom Turbine transform, as the output from today's JSX transformers output to a format designed for VDom.

Check out Surplus. It has it's own JSX transpiler, but the output is completely Surplus-specific.

Maybe Turbine could get something like this eventually.

Error message in modelView

There should be an error message if model doesn't return what view needs.

const model = ({a, b}) => {
  ...
  return Now.of({c, d});
}
const view = ({c, e}) => div(...);
const comp = modelView(model, view)();

here I expect an error, because the model doesn't return e

time.log() fails silently

It should at least error if it's not going to show you anything, but I might prefer it to show me all the milliseconds.

HTML to Turbine?

Although I like the concept of this a ton, I really think HTML is easier for visually understanding a UI hierarchy.

Would it be possible to do something like what Vue does (or JSX, though I like Vue better), to convert HTML-like templates into Turbine JS components?

input value bug

This example expects the inputs to mirror each other. This behavior works in the beginning. You can pick either input box and change the value and it will mirror in the other. But if you modify the other, it won't change in the other one. And if you go back to the first one, it won't change the other anymore. Yet, when you put the behavior in another element (such as an span), it reflects that the stream is working correctly.

import { combine, sample, changes, stepper } from "@funkia/hareactive";
import { runComponent, elements, modelView, fgo, go } from "@funkia/turbine";
const { input, span } = elements;

const tempModel = fgo(function*({ val1_, val2_ }) {
  const v = yield sample(stepper(0, combine(changes(val1_), changes(val2_))));
  return { v };
});
const tempView = ({ v }) =>
  go(function*() {
    yield span(v);
    const { inputValue: val1_ } = yield input({ value: v });
    const { inputValue: val2_ } = yield input({ value: v });
    return { val1_, val2_ };
  });
const temp = modelView(tempModel, tempView);

runComponent("#app", temp());

https://codesandbox.io/s/rrwlvx41pq

New name for `component`

The component function needs a better name.

A few ideas from the top of my head

  • createComponent
  • modelView
  • createModelViewComponent
  • mvComonent
  • statefulComponent
  • controllerComponent

How to declare attributes

This issue is meant to be a continuation of the discussion about attributes that began in #48.

The problem is that when creating an element with attributes you currently have to give them in an attrs property.

input({attrs: {placeholder: "Foo"}});

This makes declaring attributes slightly more verbose than if they could be given directly on the object. This choice was made to keep attributes separate from all the other things that one can specify on the object to an HTML element function.

This PR is meant to explore if there is a way to make attributes less verbose without sacrificing other qualities.

Confused by all the output options

I'm very confused by the various styles of output:

// 1
const counterView = ({ count }) =>
  div([
  "Counter ",
  count,
    button("+").output({ incrementClick: "click" }),
]);

// 2
const counterView = ({ count }) =>
  div([
    h2("Counter"),
    count,
    button({ output: { incrementClick: "click" } }, "Count")
  ]);

 // 3
const counterView = go(function*({ count }) {
  const {click: incrementClick} = yield button("Count")
  yield text(count)
  return { incrementClick }
})

Are some older styles that still work or are some fully depreciated? It'd probably go a long way towards my sanity if I knew which was the preferred way. (Unless they are not fully equivalent in which case I'd be curious to know the trade-offs of each style.)

Remove `of`

Jabz no longer requires the returned value in go-notation to be a monad. We should update Jabz and get rid of the now superfluous ofs.

Is combining model and view possible?

I know there are a lot of other issues discussing the pros and cons of separating the model and view. I'm simply wondering if it's currently possible to do given the current architecture, and if so, if someone could give me an example of it.

My motivation is that I like how in Reflex you can combine the model and the view seamlessly, but also separate them if you want. I like the extra freedom. The modelView function and the architecture it imposes feels a bit restrictive to me.

Embedding view into model?

I had another look at this inspiring diagram
https://github.com/funkia/turbine#adding-a-model
and got the feeling that the model really behaves like the parent to the view.

The view's both input and output only go through the model,
which exactly mirrors the child-parent component relationship:

// passed only inputs for the model
const model = ({ inputsForModel }) => {
    ...
    // the model is passing only what the view needs to know
    const view = ({ message }) => {
        return div([
            message ? `Your message is ${message}` : `Click button...`,
            button({ output: {updateMe: 'click' } }, `Click me!`)
        ])
    }
    ....
    // prepare message to show
    const message = updateMe.map(getMessage)
    ...
    // return the view with message updated
    return view({ message })
}

It looks like the model "knows" too much about view,
but I'd argue that it computes and passes the message exactly the same way as if the view were external. In fact, you could enforce it by rewriting your model as

const model =  ({ inputsForModel, view }) => ...

const view = ({ message }) => ...

const modelView = (model, view) => (...args) => model({ ...args, view })

and then reusing the same model with different views.

The advantage of this is enforcing more separation of concern,
where the view receives and emits only what is needed,
which need not be passed to the outside parents.

Also it may help to keep the view "dumb" and simple,
just passing actions and displaying the current state.
That would allow to simplify the structure by possibly entirely moving
streams and generators out of the view into the model.

It doesn't mean to use this pattern for everything,
but it may help simplifying many use cases.

What do you think?

Server Side Rendering

That would be awesome to do with Turbine. Maybe we can just run turbine stuff in jsdom, and get the output?

Maybe there could be a way to make Turbine output strings instead of actual DOM elements?

Simplify the counter example?

This seems to look like the case for the loop:
https://github.com/funkia/turbine/blob/master/examples/counters/src/index.ts#L44

That would remove the need for the model, right?
There is the conversion from stream into behaviour in the model,
I see where it comes from, but having one thing for both might possibly be simpler here?
Then it would be just one short loop :)

The component itself looks like a general purpose selector component,
to which you pass the array labels = [1, 2, 3, 4],
along with the child component selectorButton.

I wonder if that can be made into something nice and reusable...

Separating models from views

This issue is intended to be a follow-up to the discussion about the model-view separation that @jayrbolton started in #28.

One point I want to make is that I think achieving proper separation between model and view requires that the connection between the model and the view is sufficiently abstract.

Consider the following component below.

function* counterModel({ incrementClick, decrementClick }) {
  const increment = incrementClick.mapTo(1);
  const decrement = decrementClick.mapTo(-1);
  const changes = combine(increment, decrement);
  const count = yield sample(scan((n, m) => n + m, 0, changes));
  return [{ count }, { count }];
}

const counterView = ({ count }: CounterViewInput) => div([
  "Counter ",
  count,
  button({ class: "btn btn-default", output: { incrementClick: "click" } }, " + "),
  button({ class: "btn btn-default", output: { decrementClick: "click" } }, " - ")
]);

const counter = modelView(counterModel, counterView);

This is a simple counter component. The view contains an increment button and decrement button.

Let's say I want to wire up counterModel with a new view that has four buttons. Two buttons for increasing and decreasing the counter by one and two new buttons for changing the counter up and down by 5.

I can't really do that with the above model because the model has direct knowledge about the two buttons in the view. But, I can refactor the code to eliminate this:

function* counterModel({ delta }) {
  const count = yield sample(scan((n, m) => n + m, 0, delta));
  return [{ count }, { count }];
}

const counterView = ({ count }: CounterViewInput) => div([
  "Counter ",
  count,  button({ class: "btn btn-default", output: { incrementClick: "click" } }, " + "),
  button({ class: "btn btn-default", output: { decrementClick: "click" } }, " - ")
]).map(({ decrementClick, incrementClick }) => {
  return { delta: combine(incrementClick.mapTo(1), decrementClick.mapTo(-1)) });
});

const counter = modelView(counterModel, counterView);

What I've done is move the processing of the click events into the view. The model no longer has any knowledge of the detail that the view consists of two buttons. The model can be used with any view that outputs { delta: Stream<number> }.

With this change, I can easily reuse the exact same model and hook it up to the beforementioned view with 4 buttons.

const counterView = ({ count }: CounterViewInput) => div([
  "Counter ",
  count,
  button({ class: "btn btn-default", output: { increment5: "click" } }, "+5"),
  button({ class: "btn btn-default", output: { increment1: "click" } }, "+1"),
  button({ class: "btn btn-default", output: { decrement1: "click" } }, "-1")
  button({ class: "btn btn-default", output: { decrement5: "click" } }, "-5")
]).map(({ decrement1, decrement5, increment1, increment5 }) => {
  return {
    delta: combine(increment1.mapTo(1), increment5.mapTo(5), decrement1.mapTo(-1), decrement5.mapTo(-5))
  };
});

My point is: If a view does a minimal amount of preprocessing to its output then we can achieve a very loose coupling between model and view. The model expects input of a certain type. If that type contains as little knowledge about the view as possible the model will be highly reusable.

Uncomponentisation and Universalisation?

(It is perhaps by now clear that)
I find this library really interesting and inspiring.

I have never seen the use of generators in this fashion,
if just one new idea to be mentioned, among others.

I really would like to leverage the unproject
to make it even more accessible and pluggable.

So anyone can try the Turbine with a simple piece of code,
and then instantly plug and use inside any existing working application.

Uncomponentisation

Specifically, what I mean by "uncomponent" here is to enable this example

const main = go(function*() {
  yield span("Please enter an email address: ");
  const {inputValue: email} = yield input();
  const isValid = map(isValidEmail, email);
  yield div([
    "The address is ", map((b) => b ? "valid" : "invalid", isValid)
  ]);
});

to be written as pure generator with no external dependency:

const view = ({ span, input, div }) => function*() {
  yield span("Please enter an email address: ");
  const {inputValue: email} = yield input();
  const isValid = map(isValidEmail, email);
  yield div([
    "The address is ", map((b) => b ? "valid" : "invalid", isValid)
  ]);
}

It is almost the same but now can be used and tested anywhere in its purity.
It would be recognised and packaged with go internally by the Turbine
at the runtime.

Universality

What I mean by universality is to be able to plug this into any application.
Here is how the un tries to make it work:

// the only place to import from un
const createMount = require('un.js')

// configure the mount function only once in your code
const mount = createMount({ 
 
  // your favorite stream factory 
  // mithril/stream, flyd, most, or hareactive?
  createStream: require(...),
 
  // your favorite element creator 
  // mitrhil, (React|Preact|Inferno).createElement, snabbdom/h, hyperscript, 
  //  or turbine.createElement?
  createElement: require(...),
 
  // convenience helpers for your favorite 
  createTags: require(...),
 // (possibly to be renamed in createElements)
 
  // mithril.render, (React|Preact|Inferno).render, snabbdom-patch 
  //  or turbine.render?
  createRender: require(...)
})

Use the mount function to run a Turbine's component directly in DOM:

const { actions, states } = mount({ el, model, view, initState: 0 })

or embed it anywhere into any framework:

const { actions, states } = MyComponent = mount({ model, view, initState: 0 })
    ...
// inside your React Component or wherever
    ...
render = () => 
    ...
    React.createElement(MyComponent, props)
    ...

// or with JSX if that's your thing:
    ...
    <MyComponent prop1={...}  prop2={...}  prop3={...}/>
    ...

Here is an example of the working code

Here is what I think is needed to make it possible:

  • Turbine's createElement function to compile the user uncomponents into Turbine components
  • Turbine's createRender function to run the component

It would be great to hear what you think about it.

Devtools

This issue is the place for brainstorming and discussing devtools for Turbine and Hareactive. Some of the features that such a devtool may include are

  • Visualizing the dependency graph between streams and behaviors.
  • Making it possible to inspect the history of streams.
  • Time travel.
  • Manually pushing occurrences to streams and changing behaviors.

Tree shaking

This issue is to keep track of the state of tree shaking in Turbine.

We want to support tree shaking in Turbine and it's dependencies (Hareactive and Jabz). The end goal is that when users build their applications the bundle should be as tiny as possible. Users should only pay in "bundle size" for the things that they actually use. This makes it possible to add as many useful functions as possible without worrying about bloat.

Much of the necessary steps have already been taken. All our libraries ships with builds that uses the ES2015 module format which should allow for tree-shaking.

However, it is currently not possible for neither Webpack or Rollup to shake away classes as TypeScript compiles them. This makes tree-shaking almost useless as we use a lot of classes internally. The following issue microsoft/TypeScript#13721 should fix this problem.

Setting inline style?

I have tried to set {style: {color: 'blue'}} inline in the simple example:

  yield div(
  	{style: {color: 'blue'}}, 
  	[
    	    "The address is ", 
    	    map((b) => b ? "valid" : "invalid", isValid)
  	]
  );

which worked but generated some errors in the console:

ERROR in ./index.ts
(13,4): error TS2345: Argument of type '(string | Maybe<"valid" | "invalid">)[]' is not assignable to parameter of type 'Child<string | Maybe<"valid" | "invalid">>'.
  Type '(string | Maybe<"valid" | "invalid">)[]' is not assignable to type '() => Iterator<Component<any>>'.
    Type '(string | Maybe<"valid" | "invalid">)[]' provides no match for the signature '(): Iterator<Component<any>>'.
webpack: Failed to compile.

Despite of the last statement, the color was actually changed (after the necessary manual reload).


On a more general note, you seem to be using the new custom element library. Would it work with snabbdom and other libraries?

On even more general note, I wonder what be your opinion on https://github.com/uzujs/uzu/issues/2

User friendliness and other considerations

As I mentioned in this thread, I have a lot of time right now to dedicate to building out components for a frontend library/framework. I was going to change the name of flimflam to Uzu, add some improvements, and reorganize the way some of the modules are set up. However, I'd much rather join forces with another project.

I have some major hangups with Turbine, not with the concepts, but more around accessibility. For example, I would never feel comfortable introducing Turbine to the Oakland.js meetup group, because its heavy dependency on Typescript for examples, typescript signatures, and haskell-style functional abstractions would alienate the Node.js community out here.

However, I think it would be possible to basically "lift" Turbine above these more esoteric aspects. That is, what if there was a "layer" for this library's documentation that used only plain JS, no typescript, no typescript signatures, and common JS?

  • We could hide the dependency on generators
  • Make the examples basically look like flimflam, with stronger view-model separation, sort of like your celsius-fahren example that you gave me here: https://gist.github.com/jayrbolton/b38dfe8f2d8830dc01da0824f8598be1
  • We could hide or remove the peer dependency on Jabz.
  • We could hide the typescript and make everything es5 with only const and arrows (similar to the Node.js core documentation)

So this would be the "public layer" --- the topmost, immediate documentation that allows any common javascript developer to make an application.

Then, when the developer decides that they want to dig into the functional abstractions more, such as Jabz and the semantics of Hareactive, then they could view "deeper" documentation, perhaps in a wiki, that might show Typescript signatures and Haskell type classes, and so on. But these things would be under an optional "advanced" layer of documentation.

Strongly separating models from views

(This is more of an architectural consideration rather than accessibility)

In my recent experience building large-scale apps using flyd and snabbdom, we have come across one key principle that always helps in the maintainability of the app: strongly decouple Models with Views. The data tree of your markup should not try to match the data tree of your state and ui logic.

Models should even live in their own files and have no awareness of the views. Views should read from models and generate actions for them, but not much else. On examination, I think this is all very possible with Turbine. It would be nice if the documentation encouraged this style.

I actually have a real-world example of where this is important. On a single-page app, we have two sections: one section (section A) is for creating, updating, listing, and removing users, while the other section (section B) is for listing users, and for creating/updating/listing/removing nested user_roles. Both sections use different nested forms for all updates. Creating a user from section A should cause the user listing on section B to update. Likewise, creating a user_role in section B should cause the user listing in section A to update.

Most developers, using a "component" style, will have an instinct to create two separate components for section A and section B, each with their own separate models, stream logic, etc. However, they will have a terrible time trying to communicate the users back and forth between the two sections, and will find themselves with numerous cyclic dependencies, or resorting to mutating globals.

However, if that developer instead created a simple User model, and a UserRole model, which both used FRP to create/update/delete/list the users and user roles, without any markup, then they could simply pass instances of those models down through the views. The developer would never have to worry about redundant data between two siblings, cyclic data, redundant ajax logic, etc. The UI logic then becomes very easy.

The key here is that developers often have the instinct to map their data tree to their markup tree,. Say they had an html tree that generally looked like this:

markup:
  sectionA div
    table for users
    button for adding a user
    form for creating a user
  sectionB div
    table for users
    nested tables for user roles
    button for adding a role
    form for the role

Most developers would then have an instinct to create UI data and models with this structure:

data:
  sectionA
    isOpen: true/false
    users: array of user objects
    loading: true/false
    etc
  sectionB
    isOpen: true/false
    users: array of user objects
    user_roles: array of role objects
    loading: true/false
    etc

You can see how we are going to get cyclic dependencies between the sectionA model and sectionB model, because we want both sections to both read and update users

If the developer instead made their data model without thinking of the views, they would probably come up with:

data: 
  users:
    loading: true/false
    isEditing: true/false
    data: array of user objects
    user_roles:
      loading: true/false
      data: array of role objects
      etc

Here you can see that we no longer try to match the data tree to the markup tree, and we will eliminate cyclic dependencies.

Sorry if this is all too sloppily abstracted. Perhaps this real-world example would be a perfect practice test example for me to try with Turbine, so I can illustrate what I'm talking about with real code.

Take the localStorage out as parameter?

Just an idea,
motivated by the discussion in https://github.com/uzujs/uzu/issues/6#issuecomment-304519136,
maybe taking out the local Storage here as external parameter would make the data flow more explicit:
https://github.com/funkia/turbine/blob/master/examples/todo/src/TodoApp.ts#L69

Somehow I feel it does not really belong to the main app but comes from outside.

It might be interesting to compare with the implementation here:
https://github.com/briancavalier/most-todomvc/blob/master/src/index.js#L75

Lighter view creation code

I'm slightly bothered by how the code for views looks in Funnel. And I've been thinking about how to make it more "lightweight", more easy on the eyes.

Consider this code this from the continuous time example:

function* view({time, message}: ToView): Iterator<Component<any>> {
  yield h1("Continuous time example");
  yield p(map(formatTime, time));
  const {click: snapClick} = yield p(button("Click to snap time"));
  yield p(message);
  return {snapClick};
}

And compare it to what creating a virtual dom for the same thing might look like:

function view({time, message}) {
  return [
    h1("Continuous time example"),
    p(formatTime(time)),
    p(button("Click to snap time")),
    p(message)
  ];
}

To me the later is easier to read. The long keywords like yield and const adds some distracting noise I think. Haskell's do-notation would have been much nicer 😢

But besides extracting the snapTime stream the above view doesn't contain any logic. So maybe go-notation is overkill for simple cases like that.

When the above code does this:

const {click: snapClick} = yield p(button("Click to snap time"));

button returns a dictionary of all the streams and behaviors it produces. Destructuring is then used to take the stream with key click and get it out as a variable called snapClick. What if this renaming could take place inside button by giving it an object like this:

const {snapClick} = yield p(button({rename: {click: "snapClick"}, "Click to snap time"));

That looks worse than before. But by adding another new feature the view code could end up looking like this:

function view2({time, message}: ToView): Child {
  return [
    h1("Continuous time example"),
    p(map(formatTime, time)),
    p(button({rename: {click: "snapClick"}}, "Click to snap time")),
    p(message)
  ]
}

So, view2 returns a list of children that all returns an object. When such a list is used as a child our toComponent function should combine all the components (which it currently does) and also merge all the objects they return. Then the final returned object would contain a stream with the value snapClick as desired.

I not sure rename is a particularly good name for the property. But it would make creating simple views without any logic besides "destructuring-renaming" much more lightweight.

We might even build some sort of syntax for it into the fancy elements creator:

Needs a sweet logo

Since it is about time to start focusing a bit on the documentation, I think it is important to have a sweet logo. I have almost no experience with designing logos, but gave it a try:
14627919_10210844392091715_2057334774_n
I don't find this good enough as Funnel's logo, but I'll let it be the first suggestion.

Usage of OOP Classes

The OOP subject came up in this discussion and I have been wondering myself about the reasoning to use the OO classes over FP factories, where several class-style declarations and annotation would not be necessary, as far as I understand.

I am sure you guys thought well through it, just curious what were the ideas behind it.

Issues with lift

(I made this issue in Turbine because that's where I'm trying to use lift but I can move this to jabz if you prefer...)

  1. Do I import it from jabz and do lift(f, b1, b2)? Or I use it as b1.lift(f, b2)? It seems like most jabz things are importable from hareactive but not lift. Why is that?

  2. lift doesn't seem to work for 4 behaviors but the documentation says "You can also combine in this fashion any number of behaviors"

  3. lift doesn't work with loop parameters in the second argument spot with error this.fn.last is not a function:

https://codesandbox.io/s/p56qm7lkwj

const timer = loop(
  ({ x }) =>
    function*() {
      const x_ = Behavior.of(1);
      // works
      yield div(lift((a, b) => a + " " + b, Behavior.of(3), x));

      // does not work
      yield div(lift((a,b) => a + " " + b, x, Behavior.of(3)));

      return { x: x_ };
    }
);

Errors when running examples

Steps to reproduce:

git clone https://github.com/funkia/turbine/
cd turbine
yarn
cd examples/counters
yarn
npm run start

Output:

> [email protected] start /Users/dmitrizaitsev/Repos/turbine/examples/counters
> webpack-dev-server

Project is running at http://localhost:8080/
webpack output is served from /
ts-loader: Using [email protected] and /Users/dmitrizaitsev/Repos/turbine/examples/counters/tsconfig.json
Hash: 359266e80baf3486ab11
Version: webpack 2.5.1
Time: 7789ms
    Asset    Size  Chunks                    Chunk Names
bundle.js  869 kB       0  [emitted]  [big]  main
chunk    {0} bundle.js (main) 768 kB [entry] [rendered]
    [7] (webpack)/buildin/global.js 509 bytes {0} [built]
   [41] ./~/@funkia/jabz/dist/es/index.js 389 bytes {0} [built]
   [72] ./~/@funkia/hareactive/index.js 459 bytes {0} [built]
  [186] ./src/index.ts 1.64 kB {0} [built] [5 errors]
  [187] ./~/babel-polyfill/lib/index.js 833 bytes {0} [built]
  [188] (webpack)-dev-server/client?http://localhost:8080 5.68 kB {0} [built]
  [206] ./~/core-js/fn/regexp/escape.js 107 bytes {0} [built]
  [386] ./~/core-js/shim.js 7.38 kB {0} [built]
  [398] ./~/regenerator-runtime/runtime.js 24.4 kB {0} [built]
  [426] ./~/strip-ansi/index.js 161 bytes {0} [built]
  [427] ./~/url/url.js 23.3 kB {0} [built]
  [429] (webpack)-dev-server/client/overlay.js 3.73 kB {0} [built]
  [430] (webpack)-dev-server/client/socket.js 897 bytes {0} [built]
  [432] (webpack)/hot/emitter.js 77 bytes {0} [built]
  [443] multi (webpack)-dev-server/client?http://localhost:8080 babel-polyfill ./src/index.ts 52 bytes {0} [built]
     + 429 hidden modules

ERROR in /Users/dmitrizaitsev/Repos/turbine/src/component.ts
(34,14): error TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option to remove this warning.

ERROR in ./src/version4.ts
(44,3): error TS2322: Type 'Component<{} & OutputStream<{}> & BehaviorOutput<{}> & DefaultOutput>' is not assignable to type 'Component<CounterModelInput>'.
  Type '{} & OutputStream<{}> & BehaviorOutput<{}> & DefaultOutput' is not assignable to type 'CounterModelInput'.
    Property 'incrementClick' is missing in type '{} & OutputStream<{}> & BehaviorOutput<{}> & DefaultOutput'.

ERROR in ./src/version4.ts
(56,27): error TS2345: Argument of type '({incrementClick, decrementClick, deleteClick}: CounterModelInput, id: number) => IterableIterato...' is not assignable to parameter of type 'Model1<CounterModelInput, ReactivesObject, {}, number>'.
  Type 'IterableIterator<Now<Behavior<number>> | ({ count: any; } | { count: any; deleteS: Stream<number>...' is not assignable to type 'ModelReturn<ReactivesObject, {}>'.
    Type 'IterableIterator<Now<Behavior<number>> | ({ count: any; } | { count: any; deleteS: Stream<number>...' is not assignable to type 'Iterator<Now<any> | [ReactivesObject, {}]>'.
      Types of property 'next' are incompatible.
        Type '(value?: any) => IteratorResult<Now<Behavior<number>> | ({ count: any; } | { count: any; deleteS:...' is not assignable to type '(value?: any) => IteratorResult<Now<any> | [ReactivesObject, {}]>'.
          Type 'IteratorResult<Now<Behavior<number>> | ({ count: any; } | { count: any; deleteS: Stream<number>; ...' is not assignable to type 'IteratorResult<Now<any> | [ReactivesObject, {}]>'.
            Type 'Now<Behavior<number>> | ({ count: any; } | { count: any; deleteS: Stream<number>; })[]' is not assignable to type 'Now<any> | [ReactivesObject, {}]'.
              Type '({ count: any; } | { count: any; deleteS: Stream<number>; })[]' is not assignable to type 'Now<any> | [ReactivesObject, {}]'.
                Type '({ count: any; } | { count: any; deleteS: Stream<number>; })[]' is not assignable to type '[ReactivesObject, {}]'.
                  Property '0' is missing in type '({ count: any; } | { count: any; deleteS: Stream<number>; })[]'.

ERROR in ./src/version4.ts
(91,58): error TS2345: Argument of type 'Behavior<number[]>' is not assignable to parameter of type 'Behavior<number[]>'.

ERROR in ./src/version4.ts
(96,38): error TS2344: Type 'ToView' does not satisfy the constraint 'ReactivesObject'.
  Property 'counterIds' is incompatible with index signature.
    Type 'Behavior<number[]>' is not assignable to type 'Behavior<any> | Stream<any>'.
      Type 'Behavior<number[]>' is not assignable to type 'Stream<any>'.
        Property 'combine' is missing in type 'Behavior<number[]>'.

ERROR in ./src/version3.ts
(42,17): error TS2453: The type argument for type parameter 'V' cannot be inferred from the usage. Consider specifying the type arguments explicitly.
  Type argument candidate 'CounterModelInput' is not a valid type argument because it is not a supertype of candidate '{} & OutputStream<{}> & BehaviorOutput<{}> & DefaultOutput'.
    Property 'incrementClick' is missing in type '{} & OutputStream<{}> & BehaviorOutput<{}> & DefaultOutput'.

ERROR in ./src/version3.ts
(66,44): error TS2345: Argument of type 'Behavior<number[]>' is not assignable to parameter of type 'Behavior<number[]>'.
  Types of property 'child' are incompatible.
    Type 'Observer<any>' is not assignable to type 'Observer<any>'. Two different types with this name exist, but they are unrelated.

ERROR in ./src/version3.ts
(70,49): error TS2345: Argument of type '({sum, counterIds}: ViewInput) => Iterator<Component<any>>' is not assignable to parameter of type 'View1<ReactivesObject, ModelInput, {}>'.
  Types of parameters '__0' and 'm' are incompatible.
    Type 'ReactivesObject' is not assignable to type 'ViewInput'.
      Property 'counterIds' is missing in type 'ReactivesObject'.

ERROR in ./src/version2.ts
(34,17): error TS2453: The type argument for type parameter 'V' cannot be inferred from the usage. Consider specifying the type arguments explicitly.
  Type argument candidate 'CounterModelInput' is not a valid type argument because it is not a supertype of candidate '{} & OutputStream<{}> & BehaviorOutput<{}> & DefaultOutput'.
    Property 'incrementClick' is missing in type '{} & OutputStream<{}> & BehaviorOutput<{}> & DefaultOutput'.

ERROR in ./src/index.ts
(25,17): error TS2345: Argument of type '{ class: string; classToggle: { active: Behavior<boolean>; }; }' is not assignable to parameter of type 'InitialProperties'.
  Types of property 'classToggle' are incompatible.
    Type '{ active: Behavior<boolean>; }' is not assignable to type '{ [name: string]: boolean | Behavior<boolean>; }'.
      Property 'active' is incompatible with index signature.
        Type 'Behavior<boolean>' is not assignable to type 'boolean | Behavior<boolean>'.
          Type 'Behavior<boolean>' is not assignable to type 'Behavior<boolean>'. Two different types with this name exist, but they are unrelated.
            Types of property 'child' are incompatible.
              Type 'Observer<any>' is not assignable to type 'Observer<any>'. Two different types with this name exist, but they are unrelated.
                Property 'changeStateDown' is missing in type 'Observer<any>'.

ERROR in ./src/index.ts
(28,29): error TS7031: Binding element 'click' implicitly has an 'any' type.

ERROR in ./src/index.ts
(32,3): error TS2345: Argument of type '({selectVersion}: { selectVersion: any; }) => Now<{ selected: Behavior<string>; }[]>' is not assignable to parameter of type 'Model1<{}, ReactivesObject, {}, {}>'.
  Type 'Now<{ selected: Behavior<string>; }[]>' is not assignable to type 'ModelReturn<ReactivesObject, {}>'.
    Type 'Now<{ selected: Behavior<string>; }[]>' is not assignable to type 'Iterator<Now<any> | [ReactivesObject, {}]>'.
      Property 'next' is missing in type 'Now<{ selected: Behavior<string>; }[]>'.

ERROR in ./src/index.ts
(36,15): error TS7031: Binding element 'selected' implicitly has an 'any' type.

ERROR in ./src/index.ts
(43,16): error TS2345: Argument of type '() => IterableIterator<Component<Stream<"1" | "2" | "3" | "4">> | { selectVersion: any; }>' is not assignable to parameter of type 'Child<{}>'.
  Type '() => IterableIterator<Component<Stream<"1" | "2" | "3" | "4">> | { selectVersion: any; }>' is not assignable to type '() => Iterator<Component<any>>'.
    Type 'IterableIterator<Component<Stream<"1" | "2" | "3" | "4">> | { selectVersion: any; }>' is not assignable to type 'Iterator<Component<any>>'.
      Types of property 'next' are incompatible.
        Type '(value?: any) => IteratorResult<Component<Stream<"1" | "2" | "3" | "4">> | { selectVersion: any; }>' is not assignable to type '(value?: any) => IteratorResult<Component<any>>'.
          Type 'IteratorResult<Component<Stream<"1" | "2" | "3" | "4">> | { selectVersion: any; }>' is not assignable to type 'IteratorResult<Component<any>>'.
            Type 'Component<Stream<"1" | "2" | "3" | "4">> | { selectVersion: any; }' is not assignable to type 'Component<any>'.
              Type '{ selectVersion: any; }' is not assignable to type 'Component<any>'.
                Object literal may only specify known properties, and 'selectVersion' does not exist in type 'Component<any>'.
webpack: Failed to compile.

Return value is not chained?

I was expecting both elements to show up but only first one did:

function* main() {
  yield label('Please enter your name:')
  return input()
}

It does show up when I remove the yield:

function* main() {
  return input()
}

A bug or feature? :)

Why Turbine doesn't use virtual DOM

@dmitriz

Don't use virtual DOM. Instead, the created view listens directly to streams and updates the DOM accordingly. This avoids the overhead of virtual DOM and allows for a very natural style where streams are inserted directly into the view.

Could you explain this?
I've thought the point of virtual DOM was to avoid the overhead of the real one :)

In my opinion, virtual DOM solves a problem that we can completely avoid by using FRP.

Note: In this post, I'm using the words "stream" as a catch-all term for what different libraries call "stream" or "observable" and for what Turbine calls a "behavior".

Frameworks that use virtual DOM almost always represents their views as a function that takes plain state and returns virtual DOM. For instance, the render method on a React component is a function that returns a vnode tree from the components state. This is nice because it means that our view doesn't need to know which parts of our state changed and figure out what changes it then has to make to the DOM. It can just take the entire state and build a new virtual DOM. Then the virtual DOM library will find the differences and figure out how to precisely update the DOM.

When we use FRP the situation is quite different. We represent our state as streams. We can observe them and precisely know when pieces of our state changes. Based on this we can know exactly what changes to make to the DOM. We don't need a virtual DOM library to figure it out.

What I see almost all FRP/observable based frameworks do instead is something like the following: They start out with a bunch of different streams in their model, they then combine those into a single state stream and finally map their view function over the stream. So they go from state in many streams to state in a single stream to a stream of vnodes.

One very minor immediate downside to this approach is that the user has to write a bit of boilerplate code to merge the streams. Flimflam cleverly eliminates this boilerplate by automatically merging streams from an object.

The much bigger issue to the approach is that it throws information away that it then later has to get back by using virtual DOM diffing. Not only that, throwing the information away has a small overhead (the merging) and getting it back has an even bigger overhead (virtual DOM diffing).

To understand the problem let me give an example. Assume we have a model with three values, A, B and C. We express all of these as streams. We want a view with three p tags that each show one of the values.

<p>value of A here</p>
<p>value of B here</p>
<p>value of C here</p>

Whenever one of the streams changes we will have to modify the content of the p tag that it belongs to. What the above mentioned approach will do is something like the following.

const stateStream = combine(aStream, bStream, cStream);
const vnodeStream = stateStream.map(viewFunction);
// apply virtual DOM diffing to vnodeStream 

Note that when we have three streams we can observe each one of them individually and know when they change. But, when we combine them into a single stream we can only observe when any of them changes and don't know which one.

Therefore, when we apply our view function to the combined stream, it too doesn't know which value changed and it will calculate an entirely new vnode tree. Then the virtual DOM library will compare the new vnode tree with the last one it saw. It will then figure out if it was A, B or C that changed and update the view accordingly.

The virtual DOM library goes through the entire diffing process to figure out whether A, B, or C changed. But, we had that information originally before we combined the streams! If we hadn't merged the streams in the first place we wouldn't have needed that at all.

A smart enough view could simply know where each stream belongs in the DOM, observe that stream and update the DOM whenever that one changes. That is what Turbine does. In the above example, all three streams would be given to the view function and they would be inserted directly into the data structure describing the view. Then when the view is created Turbine will do something equivalent to the following

aStream.observe((a) => update first p element);
bStream.observe((a) => update second p element);
cStream.observe((a) => update third p element);

This updates the DOM just as efficiently as what a virtual DOM library would do. But it eliminates the overhead of virtual DOM diffing completely. And virtual DOM diffing does have an overhead. As an indication of that, consider React Fiber. It's a very complex solution to solve, among other things, the problem that virtual DOM diffing can in some cases be so expensive that it takes more than 16ms and causes frames to drop.

We don't have any benchmark yet. But we think the approach Turbine takes can give very good performance. In particular in cases where virtual DOM is ill-suited. I.e. animations and similar things that have to update frequently.

For instance, if you want to implement drag-and-drop in Turbine. We can represent the position of the mouse as a stream and hook that stream directly into the dragged element. Then whenever the mouse moves we will update the element position very efficiently.

To sum it up. Turbine doesn't use virtual DOM because it is unnecessary thanks to FRP. And since not using it leads to a concise way of expressing views where streams do not have to be combined but where they instead can be inserted directly into the view.

Please let me know if the above explanation makes sense and what you think about the rationale.

Dispose listeners

Remove listeners when components are removed from DOM to avoid memory leaks.

Reactive Magic

@ccorcos

This is an answer to paldepind/flyd#142 (comment).

Again, thank you for the feedback and thank you for taking the time to take a look at Turbine. It is really useful to get feedback like that and I'm grateful that you're shaing you opinion 😄

Spent some more time looking through Turbine this morning... That's some pretty intense stuff! It's very well thought-out, but building a mental model for how it all works is pretty challenging. I think if you included explicit type annotations in the tutorial, it would be a lot easier to pick up on. I think it might also help me understand how everything works if you had an example that showed me how to get all the way down to the actual DOM node where I could do things like call scrollTo() or instantiate a jQuery plugin or something.

For using a jQuery plugin we'd probably have to create a mount hook that gives the raw element. We haven't done that yet though. Regarding scrollTo I'll get back to you with an example.

I see what you mean about the differences though. It actually is a bit different. No selectors is 👍 and the code is really clean. I'm still trying to figure out where the challenges will be...

Let me know if you figure it out 😄. We have tried to make Turbine as powerful as possible. There are some functional frameworks that achieve purity by limiting what one can do. In Turbine we have tried to create an approach that is pure but without making things harder.

If you had a global application state for sidebarOpen that you needed to access in many places, I'm assuming this would just be a behavior that you can just import and combine in a model function? It wouldn't be pure though, right?

Turbine is completely pure. The answer to the question "is it pure" should always be "yes". sidebarOpen would probably be created inside a component and then the component would have to pass it down the children that need it.

You wouldn't be able to just import it. We can do that with a few behaviors. For instance, the mouse position, the current time and keyboard events can simply be imported. That is because they "always exist" in the browser. But sidebarOpen would be created inside some component so it can't be a global behavior that can be imported.

To stretch this abstraction even further, I might want to have two counters: the first counter has a delta of 1, and the second counter has a delta of the value of the first counter. Here's how I would do it using reactive-magic:

That is a good example. Here is how one could write that using Turbine.

const counterModel = go(function* ({ incrementClick, decrementClick }, { delta }) {
  const increment = snapshot(delta, incrementClick);
  const decrement = snapshot(delta.map(n => -n), decrementClick);
  const changes = combine(increment, decrement);
  const count = yield sample(scan((n, m) => n + m, 0, changes));
  return { count };
});

const counterView = ({ count }) => div([
  button({ output: { decrementClick: "click" } }, "dec"),
  span(count),
  button({ output: { incrementClick: "click" } }, "inc")
]);

const counter = modelView(counterModel, counterView);

const app = go(function* () {
  const { count } = yield counter({ delta: Behavior.of(1) });
  yield counter({ delta: count });
});

You can check out the example live here.

What intrigues me so much about this example is how clean the mental model is. It feels very easy to make sense of to me.

I think your example is nice. But, I think the one I wrote in Turbine is even better. I think the Turbine code avoids some problems. Problems that I see in many frameworks and some we've particularly tried to avoid in Turbine. Here are some of the problems.

  1. The definitions lie. For instance, the line delta = new Value(1) doesn't actually tells me what delta is. It says that delta is equal to 1 but that obviously isn't the entire truth. This means that if I want to know what delta actually is I'll have to find all the lines that use delta. In a real app that can be hard.
  2. When I look at the inc method I cannot see who may call it. It might be called once, twice, or many times in the view. This makes it hard to figure out when exactly the side-effect that inc has is triggered.
  3. The input to counter is mixed with the output from the counter. For instance, this line:
<Counter count={this.delta} delta={1}/>

It looks like both count and delta are input to the component. But, since Counter calls
update on count it actually uses it as an output channel. This means that whenever a Value instance is passed to a component it's hard to know if it's actually input or output.

I apologize for being a bit hard on your example 😅 The problems I pointed out are found in most React code. Let me explain how Turbine avoids them.

  1. In Turbine definitions always tells the entire truth. Every time a line begins with const the definition tells everything about the defined value. This makes it very easy to look at the code and figure out what things are.
  2. There are not impure methods in Turbine. You never looks at something and wonder "who are actually triggering this code?". The code always clearly describes where things are coming from.
  3. In Turbine a component function takes all its input as arguments and all its output is part of its return value. This makes a lot of sense. Input is arguments to functions and output is returned from functions. For instance, in the Turbine example, counter is a function that returns Component<{count: Behavior<number>>}.

I think the properties I described above makes Turbine code easy to understand and will make it scale really well with increasing complexity.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.