Fork me on GitHub
#fulcro
<
2024-01-31
>
Quentin Le Guennec09:01:51

Hi! Is there a page explaining the reasons behind the creation of fulcro, the issues it aims to solve compared to standard approaches like re-frame/redux ?

Brett15:01:41

Hey you might want to watch this video by Tony Kay : https://www.youtube.com/watch?v=_g_Kyl4_TSA

tony.kay15:01:22

You might try listening to the clojurescript podcast episodes where I was a guest.

tony.kay18:01:14

but here’s a short list: • I wanted a consistent way to write full-stack apps where the main applications was as pure a function of state as possible. In Fulcro the View = F(state) in a very literal sense. No magic (people claim it is too magical, but in fact there is zero magic…F is literally your query run against the database, and then passed to your (pure, or mostly pure) render of that state). • I wanted a clean way to keep track of the “state of the world right now” as an immutable value such that all manipulations of the applications “state” were purely functional. Thus, a normalized database in an immutable map stored in an atom. Application progression is ALWAYS a swap! against the “state of the world” using a pure function to get the “new state of the world”. It’s as close to pure functional programming as you can get, meaning that most application logic is trivial to unit test. There are no “reducers” on the side, event systems mucking with things in a hard-to-trace chain of events. There is literally no magic. The model is extensible, so you can add in these kinds of things if you wanted to (see YouTube video on integrating re-frame with Fulcro), but I’ve written half a million lines of applications in Fulcro at this point, and have not really found the need. • Sane integration with outside data sources. I wanted the I/O to be a clear separate layer, since it has to side-effect and often deals with mutability. I also wanted the language for this integration to be relatively rich (EQL gives very targeted selection of data request, and mutations cleanly represent the abstraction of the desire to make a change). Full-stack apps are distributed systems, so having a designed (and not evolved by the seat of your pants) model for transactional interactions with the outside world adds a huge boost to your ability to manage and reason about external interactions (from network servers to web workers). You need things like: predictable order of operation, predictable merge behaviors, the ability to cancel network operations easily, the ability to see what operations are running in what order (e.g. the transaction log in Fulcro Inspect). Some other non-obvious examples of where this comes in handy: I’ve written things like a remote to run the Etherium Solidity compiler as a web worker. The source is submitted via fulcro’s transact!, and the mutation involved has a structured “returning” that lets me receive back the result as structured data that is auto-normalized into the app state. Futhermore, this “remote” can send me progress updates (so I can watch the compiler working in the UI), again using auto-normalized integration. The mutation in the client side literally just says what it expects the “remote” to return as a normalizing component query. There is almost zero code on the client side. The majority of that code is in the web worker, which was about 200 lines of code if I remember right. • All that said, I then added UI state machines, and now statecharts (both purely functional in most of their observable behavior) for helping organize larger conceptual designs that involve many “actors” (UI components, usually). For example, my “session management and onboarding” for one of my commercial apps is a statechart that takes a couple thousand lines of CLJC.

❤️ 1
Quentin Le Guennec21:01:40

Thank you so much about the detailed answer and the video. Quick question, don’t you think a statically typed language would be a better fit for fulcro ? Strong types have a huge benefit when writing/using frameworks. I guess you you didn’t choose a language for fulcro but rather chose to make a framework for clojurescript, but I’m still curious about it.

tony.kay03:02:23

Quite the opposite: Clojure(script) is a big feature IMO. Types can help with performance, and sometimes self-documentation. But the benefits of Clojure(script), IMO. are huge. I do mourn the fact that it is harder to staff for Clojure, but in terms of “types make it better”, I would generally disagree. Yes, autocomplete works better in statically-typed systems, but the added complexity you get from the is a huge disadvantage. Not to mention the benefits of CLJC, clean macros, data-driven architectures without the plethora of “wrapper classes”, and immutable data with full-support for literals. I can see HOW to do Fulcro in statically-typed languages, and yes it would be more popular there, but can you imagine the amount of boilerplace to get a simple EQL query in one of those languages? I’ve got some adapters I’ve written in C# for EQL. They are type safe. They are ugly, and the syntax ends up looking like this for simple things like queries:

var query = m.Event.allEvents.join(m.Event.id,
                                         m.Event.title,
                                         m.Event.startDate,
                                         m.Event.endDate,
                                         m.Event.useAltitude,
                                         m.Event.defaultVisibility,
                                         m.Event.quickSearchTags.join(m.Tag.id,
                                                                      m.Tag.backgroundColor,
                                                                      m.Tag.icon.join(m.Image.id,
                                                                           m.Image.sha)),
where I have to declare things like Event like this:
public struct Event {
    public static readonly IDAttr id = new IDAttr("event/id");
    public static readonly StringAttr title = new StringAttr("event/title");
    public static readonly DateAttr startDate = new DateAttr("event/start-date");
    public static readonly DateAttr endDate = new DateAttr("event/end-date");
    public static readonly BoolAttr useAltitude = new BoolAttr("event/use-altitude?");
    public static readonly DoubleAttr defaultVisibility = new DoubleAttr("event/default-visibility");
    public static readonly TimeZoneAttr zoneInfo = new TimeZoneAttr("event/timezone");
    public static readonly ToManyJoinAttr anchors = new ToManyJoinAttr("event/anchors", Anchor.id);
    public static readonly ToManyJoinAttr quickSearchTags = new ToManyJoinAttr("event/quick-search-tags", Tag.id);
    public static readonly ToManyJoinAttr allEvents = new ToManyJoinAttr("event/all", id);
  }
and I have to define all these types to say what I want about a given attribute
public class StringAttr : Attr<string> {
    public StringAttr(string name) : base(name) { }

    public override object fromJSON(JSONNode container) {
      return container[name]?.Value;
    }

    public override object fromBSON(BsonValue container) {
      return container.AsString;
    }

    public override BsonValue toBSON(object e) {
      return (string)e;
    }
  }
which has a mess of a type hierarchy.

tony.kay03:02:18

Yes, it “works”, but RAD’s use of simple maps to say EVERYTHING YOU COULD WANT EVER about an attribute is much cleaner, more easily extended/enriched, and many lines of code shorter.

tony.kay03:02:02

I don’t “hate” the typed version. I just consider it inferior in many ways. It has some advantages as well. When I make a change the type system does help catch some trivial kinds of errors.

tony.kay03:02:19

but I also have to struggle with the type system when I need to evolve the system. The work it saves me almost always costs me the same or more in other hassles…and I’m spending a bunch of brain cells on types, syntax, and complex design, when all I really need is the data.

tony.kay03:02:57

I’ve been programming in C/C++/Java/Perl/C#/Objective-C/Scala/Clojure/Clojurescript for almost 40 years. 33 of that in typed languages. I’ve got a feel for them 😄

tony.kay04:02:20

Anyway, all that said, the C# stuff I did (I’m doing some Unity work) is interfacing with a Fulcro back-end with EQL and is using a “client side” (C#) normalized database with websockets. The concepts all work well there, and I have been tempted to flesh it out more in type-land. Certainly more popularity lies there, but I built Fulcro for me and my purposes, and it serves me very very well as-is.

tony.kay04:02:14

I’d be most tempted by a language that is typed BUT has immutable literals like Clojure. (sets maps vectors). You still end up working a lot with “object” when you do data-driven programming and try to avoid the dizzying array of incidental artifacts. I’ve considered kotlin, OCAML, and Scala…having a JVM AND JS compile target would also be needed, and at the moment the tooling for everything else (using a unified language for front/back) is pretty poor, unless you want to use node for you back-end, which I dont’.

tony.kay04:02:45

I’ve not seen any tooling that rivals CLJS with it’s clean hot reload for Fulcro (and friends whose immutable state-of-the-world) keeps you solidly in place while developing.

tony.kay04:02:03

and if I ever get around to finishing Guardrails Pro, I think we can do many things better than types anyway

Quentin Le Guennec07:02:10

Very interesting, thanks. I am right now building some UI for unity (with UI toolkit), and I’m also finding myself reusing some concepts I’ve discovered in fulcro. (I wouldn’t say it’s close, but we got some things like UISM, de-normalized database, although the queries are built with UniRX). I agree with all your points about static typing, but I also think overall it decreases the cognitive load for me. But props to you really, I think your framework is right in many ways, and i couldn’t have come up with those concepts myself.

Quentin Le Guennec07:02:36

For EQL maybe the Roslyn code generator would do the trick, but I’m feeling like records used like Haskell sum types would also maybe work