Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Preferred way to represent "not yet known" value #85

Open
sajagi opened this issue Jul 3, 2024 · 6 comments
Open

Preferred way to represent "not yet known" value #85

sajagi opened this issue Jul 3, 2024 · 6 comments

Comments

@sajagi
Copy link
Contributor

sajagi commented Jul 3, 2024

Hi,

What is the canonical way to represent "not yet known" value? For example, result of an async expression?

The simplest way (achievable now) is to simply use Store<T option>.

Stores and their derivations are implementations of IObservable<T>, which do not guarantee that values are always present (that is, the observable is not "behavior subject", as described here. This would most likely work with current functions (e.g. Bind.el), however, there is no way to represent "loading" state (or is there?). A possibility would be to add a new function overload with loader element to be displayed when value is not available yet.

What are your thoughts on this?

@davedawkins
Copy link
Owner

Hi,
I use Bind.promise for this. Here's one example of usage:

https://sutil.dev/#examples-await-blocks

That's interesting to read about BehaviorSubject. Store does have an initial value, but you're right that as soon as you start projecting and filtering, you end up with IObservable. (Store does guarantee to send the initial value out to any new subscribers though)

Here are the Bind.promise functions:

    static member promises (items : IObservable<JS.Promise<'T>>, view : 'T  -> SutilElement, waiting: SutilElement, error : Exception -> SutilElement)=
        Bind.el( items, fun p -> Bind.promise(p, view, waiting, error) )

    static member promise (p : JS.Promise<'T>, view : 'T  -> SutilElement, waiting: SutilElement, error : Exception -> SutilElement)=
        Bind.el(  p.ToObservable(), fun state ->
            match state with
            | PromiseState.Waiting -> waiting
            | PromiseState.Error x -> error x
            | PromiseState.Result r ->  view r
        )

    static member promise (p : JS.Promise<'T>, view : 'T  -> SutilElement) =
        let w = el "div" [ CoreElements.class' "promise-waiting"; text "waiting..."]
        let e (x : Exception) = el "div" [ CoreElements.class' "promise-error"; text x.Message ]
        Bind.promise(p, view, w, e )

@sajagi
Copy link
Contributor Author

sajagi commented Jul 4, 2024

Thanks!

I see you use a similar approach to the 'T option one (with the ability to handle errors). I tweak my store initialization so I can directly modify the model in the store (plus wrapping it in Some). Error handing is usually done upstream (or I use Result):

module Store = 
   let makeAsyncOption (x: Async<'T>) : IStore<'T option> =
        let store = Store.make None
        async {
            let! result = x
            result |> Some |> Store.set store
        } |> Async.StartImmediate
        store

Suggested variant would be to use a custom store-like class implementing IObservable<'T>, that would not have an initial value. Set would work the same (and would initialize the store), Modify would trigger an error when called in uninitialized state, Value would become TryGetValue. The upside is that the consumers would have no notion of its "lazy" flavor, be it promise, async or something completely else. I wonder what the repercussions would be. It probably needs to be put to test ;)

@davedawkins
Copy link
Owner

davedawkins commented Jul 6, 2024

I'm open to this, but I need to understand why it would be desirable to have this extra code to maintain when we can already implement Store<'T> with 'T = Option<'U>
I think it's because I haven't understood your use case properly, and why using Option<> isn't expressive enough.

@sajagi
Copy link
Contributor Author

sajagi commented Jul 11, 2024

It is more about what is the behavior of Bind.el and similar methods / functions for IObservable<> which does not provide a value right away. Instances of IObservable do not have to be necessarily derived from Store (for any reason) and having no initial value is not forbidden by the interface contract. Currently, any binding to empty IObservable becomes a SideEffect, which is in practice similar to Html.none (?).

One could take advantage of this behavior and use it for "value not yet available". The extensions which would allow more user-friendly API (creating stores without initial value, having Bind.el with alternative "loader" element, etc.) do not even have to be inside the core library.

On the other hand, this means any current and future usages of IObservable must never rely on a value present (for example, checking the subscriber has been called by the time the call of Subscribe is finished).

edit: typos

@davedawkins
Copy link
Owner

davedawkins commented Jul 11, 2024

I see. It's not so much about the way Store behaves, but consumers of IObservable. It's true that in Sutil, I know that when I'm handed an IObservable, and subscribe to it, that I will immediately receive the initial value (because of the way Store works, which I modelled on Svelte's stores/cells).

Would it be useful then to have overloads of Bind.el (et al) like the following?

// Current signature (or at least, very close to it
Bind.el( data : IObservable<'T>, view: 'T -> SutilElement )   

// Using Option
//  view None will be shown until a value arrives
Bind.el( data : IObservable<'T>, view: 'T option -> SutilElement ) 

// Without Option, using init/view pair
//  init() will be shown value arrives
Bind.el( data : IObservable<'T>, init: unit -> SutilElement, view: 'T -> SutilElement ) 

Option feels more canonical, but I don't like that we have to wrap every value in Some just to handle an initial case.

Regardless of that small point, am I addressing the issue you're raising?

Cheers

@sajagi
Copy link
Contributor Author

sajagi commented Jul 11, 2024

Yes, it's exactly what I had in mind. Sorry it took me several posts to make myself clear :)

Unless you have some prior objections to the suggested behavior, I'll test it in one of my projects to see how it performs in real scenarios. Then you should be able to write something like:

let store : IObservable<_> = getDataAsync() |> DelayedStore.makeAsync

// ...

Bind.el(store, (fun () -> Html.text "Loading...."), (fun data -> Html.text $"Data received: {data}"))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants