rescript-lang / rescript-compiler

The compiler for ReScript.
https://rescript-lang.org
Other
6.65k stars 442 forks source link

Understanding @bs.string with variants #584

Closed ghost closed 8 years ago

ghost commented 8 years ago

I have some OCaml code like this:

external on : t Js.t -> ([ `quit of (Event.t -> int -> unit [@bs]) ] [@bs.string]) -> t Js.t = "" [@@bs.send]

It is used to generate Javascript code like this:

app.on("quit", function(event, code) {
  …
});

I have a couple of questions about how @bs.string works which I list below.

First, @bs.string only seems to generate strings if the polymorphic variant types are given "inline", directly with the function type signature. If I try to specify a separate type, say like this:

type event = [ `quit of (Event.t -> int -> unit [@bs]) ] [@bs.string]

… and then specify the type of on like this:

external on : t Js.t -> event -> t Js.t = "" [@@bs.send]

… then the code still compiles but the constructors for event are generated as numbers rather than strings as I would expect.

First question: is it possible to get the constructors generated as strings for the non-inline type event when compiling on? This would make the code cleaner and easier to read I think.

Second question: is it possible to get @bs.string working for extensible (GADT) variants?

One case I have in mind is the emit method:

type _ event = .. [@bs.string]
external emit : t Js.t -> 'e event -> bool = "" [@@bs.send]
val obj : t

type appEvt (* application specific *)
type _ event += Quit : int -> appEvt event [@bs.as "quit"]
emit obj @@ Quit 0

… which should be translated like this:

obj.emit("quit", 0);

I think the reason you'd want to be able to do this with extensible variants rather than polymorphic variants is so that you could specify a certain set of allowed events in your application and then ensure that they are always used consistently. AFAICT this wouldn't work with just polymorphic variants.

Third question: why does @bs.string with polymorphic variants (in the first example with on) get translated the way it does to Javascript? It appears that in the translation the arguments to the constructor are spliced into the arguments of the function. The way it works is very useful but I found it surprising when I first saw it. Or is that just part of the behavior of @@bs.send?

bobzhang commented 8 years ago

hi, @freebroccolo , indeed, in external type declarations, type expressions are not first class, you have to write the full types there, this also true in C bindings. So here, [@bs.string] will have effect only when it is declared as external, but it is sound as long as your external is correct (so still need careful review of your externals).

But we can improve in error message, we should emit a warning, since the attribute is not used below:

type event = [ `quit of (Event.t -> int -> unit [@bs]) ] [@bs.string]

We can improve in the external type declaration here:

external on :
  t Js.t -> 
  ([ `quit of (Event.t -> int -> unit [@bs]) ] [@bs.string] [@bs.as: event]) -> t Js.t = "" [@@bs.send]

so that it will generate code like this

type event  = [`quit of Event.t -> int -> unit [@bs]]
external on :
  t Js.t -> event -> t Js.t = "" [@@bs.send]

Do you have a use case for it? I am not sure the benefit is high enough to justify such syntactic sugar

Polymorphic variant is more flexible than extensible variant, it is not only extensible but also support subtyping...

About your third question, yes, it relies on compiler magic for good reasons(event listener API is everywhere in nodejs), but we do it very careful, so as long as your external is declared correct, it is sound.

ghost commented 8 years ago

@bobzhang Thanks for the response.

hi, @freebroccolo , indeed, in external type declarations, type expressions are not first class, you have to write the full types there, this also true in C bindings. So here, [@bs.string] will have effect only when it is declared as external, but it is sound as long as your external is correct (so still need careful review of your externals).

But we can improve in error message, we should emit a warning, since the attribute is not used below:

Okay, that clears it up. It would be nice if there were some way to lift that restriction for external if possible but otherwise having a clear error message would help tremendously.

We can improve in the external type declaration here:

external on : t Js.t -> ([quit of (Event.t -> int -> unit [@bs]) ] [@bs.string] [@bs.as: event]) -> t Js.t = "" [@@bs.send]`

so that it will generate code like this

type event = [quit of Event.t -> int -> unit [@bs]] external on : t Js.t -> event -> t Js.t = "" [@@bs.send]`

Do you have a use case for it? I am not sure the benefit is high enough to justify such syntactic sugar

Well, the real use for it would be using it for extensible variants (more later).

But with polymorphic variants, I suppose it could be useful for improving readability. I am working on bucklescript bindings for some complicated APIs (currently electron and vscode) and some methods have a large number of cases (here's a short example). Encoding the variants inline will make the method type take a lot of space.

I don't know if that use case is very convincing though.

The main reason I'd like this feature is for something like the following code. It uses extensible variants instead of polymorphic variants specifically because extensible variants support GADT constructors. As far as I know, polymorphic variants do not.

I agree that polymorphic variants are generally the better choice but there are some patterns where you really need GADTs to provide a nicely typed API.

In the following code, I define an EventEmitter class type and specify that the on and emit methods take a value of type ('namespace, 'args) event, where event is an extensible GADT variant. The constructors for event should be translated to JS as just strings, but the extra type information in the parameters can be used to constrain subsequent arguments to the method.

Next I define some class types that inherit from EventEmitter but which have a more constrained type for emit and on. This makes it possible to specify groups of events specific to subclasses of EventEmitter which is very useful for specific applications (e.g., electron).

I don't think it's possible to encode this pattern just with polymorphic variants. Perhaps there is a way but if so I think it would probably involve coercions and extra annotations somewhere which would make it more difficult to use.

(* First we define the EventEmitter class type. Later we can inherit from
   this class and restrict the kind of events to handle. *)
module EventEmitter = struct
  type ('namespace, 'args) event = .. (* we want the constructors to be translated with [@bs.string] *)
  class type ['namespace] t = object
    method emit : ('namespace, 'args) event -> 'args -> 'namespace t
    method on : ('namespace, 'args) event -> ('args -> unit) -> 'namespace t
  end
end

(* Foo is an EventEmitter. It handles `Foo events. *)
module Foo = struct
  type namespace = [`Foo]
  type (_, _) EventEmitter.event +=
    | Echo : (namespace, string) EventEmitter.event
  class type t = object
    inherit [namespace] EventEmitter.t
  end
end

(* Example calling #emit and #on for Foo *)
let foo : Foo.t = failwith ""
let _ = foo#emit Foo.Echo "welp"
let _ = foo#on Foo.Echo begin fun msg ->
    Printf.printf "%s\n" msg
  end

(* Bar is another EventEmitter. It handles `Bar events. *)
module Bar = struct
  type namespace = [`Bar]
  type (_, _) EventEmitter.event +=
    | EchoTimes : (namespace, < msg: string; count: int option >) EventEmitter.event (* [@bs.as "echo-times"] *)
  class type t = object
    inherit [namespace] EventEmitter.t
  end
end

(* Example calling #emit and #on for Bar *)
let bar : Bar.t = failwith ""
let _ = bar#emit Bar.EchoTimes @@ object method msg = "welp" method count = Some 42 end
let _ = bar#on Bar.EchoTimes begin fun obj ->
    let count = begin match obj#count with
      | None -> 1
      | Some n -> n
    end in
    for i = 0 to count do
      Printf.printf "%s\n" obj#msg
    done
  end

(* the following causes a type error *)
(* let _ = bar#emit Foo.Echo "welp" *)
bobzhang commented 8 years ago

@freebroccolo Quick answer to address this issue:

external on
    : t Js.t
      -> ([ `accessibilitySupportChanged of (Event.t -> bool -> unit [@bs]) [@bs.as "accessibility-support-changed"]
         ..
          ] [@bs.string])

below should still work

type accessibleity_support_changed_event_cb = Event.t -> bool -> unit [@bs]
external on : t Js.t -> 
      ([ `accessbilitySupportChanged of  accessibleity_support_changed_event_cb [@bs.as "xx"] 
       ..
     ] [@bs.string]) -> unit = "" [@@bs.send]

I will get back to you on extensible vairant later

ghost commented 8 years ago

@bobzhang okay, that could help some I think. Actually I just realized that one good example of where it could be useful is for implementing on and once. If you specify these individually, you'd basically need to duplicate a bunch of the types for both methods even though I think they should always be the same.

As for the example with extensible variants, one thing I should add is that the key issue is being able to specify precisely which events are supported for an EventEmitter subclass. So supporting inheritance is the key here and this was the only clean pattern I could figure out to make it work.

I do think its possible to also support subtyping for the events this way since the 'namespace parameter controls which events are allowed. But I haven't experimented with that much yet.

ghost commented 8 years ago

@bobzhang Here's a better example of actual code I'd like to write using this pattern:

[@@@bs.config{no_export}]

module EventEmitter = struct
  type ('k, 'v) event = .. (* [@bs.string] *)
  type (_,_) event +=
    | NewListener : ([> `EventEmitter], string) event [@bs.as "newListener"]
    | RemoveListener : ([> `EventEmitter], string) event [@bs.as "removeListener"]
  class type ['k] t = object
    method emit : ('k, 'v) event -> 'v -> 'k t Js.t
    method on : ('k, 'v) event -> ('v -> unit) -> 'k t Js.t
    method once : ('k, 'v) event -> ('v -> unit) -> 'k t Js.t
  end [@bs]
end

module BrowserWindow = struct
  class type t = object
    method loadURL : string -> unit
  end [@bs]
end

module Certificate = struct type t end
module Event = struct type t end
module LoginAuthInfo = struct type t end
module LoginRequest = struct type t end
module WebContents = struct type t end

module App = struct
  type ns = [`App | `EventEmitter]
  type (_,_) EventEmitter.event +=
    | AccessibilitySupportChanged : (ns, Event.t * bool) EventEmitter.event [@bs.as "accessibility-support-changed"]
    | Activate : (ns, unit) EventEmitter.event [@bs.as "activate"]
    | BeforeQuit : (ns, Event.t) EventEmitter.event [@bs.as "before-quit"]
    | BrowserWindowBlur : (ns, Event.t) EventEmitter.event [@bs.as "browser-window-blur"]
    | BrowserWindowCreated : (ns, Event.t * BrowserWindow.t) EventEmitter.event [@bs.as "browser-window-created"]
    | BrowserwindowFocus : (ns, Event.t * BrowserWindow.t) EventEmitter.event [@bs.as "browser-window-focus"]
    | ContinueActivity : (ns, Event.t * string * 'a Js.t) EventEmitter.event [@bs.as "continue-activity"]
    | GpuProcessCrashed : (ns, unit) EventEmitter.event [@bs.as "gpu-process-crashed"]
    | Login : (ns, Event.t * WebContents.t * LoginRequest.t * LoginAuthInfo.t * (string * string -> unit)) EventEmitter.event [@bs.as "login"]
    | OpenFile : (ns, Event.t * string) EventEmitter.event [@bs.as "open-file"]
    | OpenURL : (ns, Event.t * string) EventEmitter.event [@bs.as "open-url"]
    | Ready : (ns, unit) EventEmitter.event [@bs.as "ready"]
    | SelectClientCertificate : (ns, Event.t * WebContents.t * string * Certificate.t list * (Certificate.t -> unit)) EventEmitter.event [@bs.as "select-client-certificate"]
    | Quit : (ns, Event.t * int) EventEmitter.event [@bs.as "quit"]
    | WebContentsCreated : (ns, Event.t * WebContents.t) EventEmitter.event [@bs.as "web-contents-created"]
    | WillFinishLaunching : (ns, unit) EventEmitter.event [@bs.as "will-finish-launching"]
    | WillQuit : (ns, Event.t) EventEmitter.event [@bs.as "will-quit"]
    | WindowAllClosed : (ns, unit) EventEmitter.event [@bs.as "window-all-closed"]
  class type t = object
    inherit [ns] EventEmitter.t
    method getVersion : unit -> string
    method quit : unit -> unit
  end [@bs]
end

module Electron = struct
  external app : App.t Js.t = "" [@@bs.module "electron"]
  external mkBrowserWindow : unit -> BrowserWindow.t Js.t = "BrowserWindow" [@@bs.new] [@@bs.module "electron"]
end

module Example = struct
  let go = fun [@bs] () ->
    let app = Electron.app in
    app##begin on EventEmitter.NewListener begin fun (msg) ->
        Js.log msg
      end
    end##begin on App.Ready begin fun () ->
        let win = Electron.mkBrowserWindow () in
        win##loadURL "../example/index.html"
      end
    end##begin on App.WillFinishLaunching begin fun () ->
        Js.log (app##getVersion ());
        app##quit ()
      end
    end
end

let _ = Example.go () [@bs]

Edit: And here is the code it generates, which would be perfect if Ready and WillFinishLaunching were defined as the appropriate strings:

// GENERATED CODE BY BUCKLESCRIPT VERSION 0.8.8 , PLEASE EDIT WITH CARE
'use strict';

var Caml_exceptions = require("bs-platform/lib/js/caml_exceptions");
var Electron = require("electron");

var NewListener = Caml_exceptions.create("NewListener");

var Ready = Caml_exceptions.create("Ready");

var WillFinishLaunching = Caml_exceptions.create("WillFinishLaunching");

function go() {
  var app = Electron.app;
  return app.on(NewListener, function(msg) {
    console.log(msg);
    return /* () */ 0;
  }).on(Ready, function() {
    var win = new Electron.BrowserWindow();
    return win.loadURL("../example/index.html");
  }).on(WillFinishLaunching, function() {
    console.log(app.getVersion());
    return app.quit();
  });
}

go();

/* go Not a pure module */
ghost commented 8 years ago

Okay, I've also updated the improved example to show how you can handle subtyping for the events. App can handle the basic NewListener and RemoveListener events from EventEmitter (from the Node API) in addition to it's own special events.

bobzhang commented 8 years ago

the challenging part is to support bs.as, there is no easy way to pass such information, but it is necessary to make it really useful.

I am thinking that actually we can make this work

type event = [ `quit of (Event.t -> int -> unit [@bs]) ] [@@bs.string]

external on : t Js.t -> (event [@bs.string]) -> unit = "" [@@bs.send]
external once : t Js.t ->  (event [@bs.string]) -> unit = "" [@@bs.send]
ghost commented 8 years ago

the challenging part is to support bs.as, there is no easy way to pass such information, but it is necessary to make it really useful.

Can you say a little more about where the difficulty is? I basically know nothing about how these annotations work internally but I'd like to try and understand. Is it a problem specific to the extensible variant constructors? How does it work for the polymorphic variants?

I am thinking that actually we can make this work

type event = [ `quit of (Event.t -> int -> unit [@bs]) ] [@@bs.string]

external on : t Js.t -> (event [@bs.string]) -> unit = "" [@@bs.send] external once : t Js.t -> (event [@bs.string]) -> unit = "" [@@bs.send]

This would be a really great improvement so it'd excellent if you can do it.

However, I do think that in the long term it's really worth trying to get something like the extensible variant version working too. The main reason I think so is because it seems to me that trying to accurately model various APIs (e.g., Node and others) is going to be very difficult without making fairly heavy use of inheritance.

This is the problem I am currently encountering with external and bs.send, which you can see in the first link I posted to the electron example. Basically for each method I have to choose whether to model it as part of the class type or make it an external. But this makes using it kind of awkward.

Given that choice, it almost seems better to just use external everywhere and not use class type. But then that makes it much harder to declare high quality reusable bucklescript bindings that closely match the JS APIs. Especially with ES6 becoming more popular, use of classes and inheritance is becoming much more widespread.

Maybe there is a way to avoid classes and use first-class modules instead but I suspect that will be more difficult and the generated code will probably not be as nice.

What do you think?

bobzhang commented 8 years ago

are you aware that polymorphic variant you can also do the composition?

type u = [ `x ]
type v = [ |  u | `xx]

does this fit your need? btw, for FFI, you can mix OO style bindings with functional bindings

bobzhang commented 8 years ago

it is hard to explain some parts are easy to do while some parts are hard to do. the high level explanation is that we reused most existing ocaml compiler infrastructure, at the same time, we want to avoid patches as much as I can. We own the front-end (without types), external declaration and lambda expression (no types), so unless we do aggressive patches to the compiler, bs.as is hard to support.

but it is quite easy to do this in the type checker for example (cc @alainfrisch, @garrigue)

type t += XX of string [@as  "event-name"] (* the bytecomp encode it as this string instead of "XX"*)
ghost commented 8 years ago

@bobzhang thanks for the overview and taking the time to explain this stuff to me :)

About polymorphic variant composition, it is useful but I don't think it is enough on it's own to support the patterns I'm thinking of. But I may be overlooking something.

Basically what I would like to do is avoid using external and bs.send for methods and only use it to specify things like globals. I'd like to use class type for as much as possible.

What I have in mind is to start putting together some comprehensive bucklescript bindings for Node and several other packages. If you look at some examples, like the flow definitions for Node or the typescript definitions for electron there's pretty heavy use of classes and inheritance.

Unless I'm misunderstanding something, I don't quite see how we could build those kind of definitions yet with bucklescript because it seems like you will need to use external for any of the methods where you need polymorphic variant constructors translated to strings. So not just for event handling but also for methods like this one.

From what I understood of your first comment, bs.string only works with external so this couldn't be done as a method, is that correct? But in that case, you won't be able to put it in a class type that you can inherit from.

Using this specific example from crypto$Cipher.final, if you wanted to work with a JS class that extended crypto$Cipher (e.g., class Foo extends crypto$Cipher) in bucklescript, I think you'd have to have a separate external final_foo.

So the perspective I'm thinking of is not so much about mixing method and external (i.e., the OO and fp approach) within my application but how do the bindings in a general way so that I could develop a bucklescript library (eventually for bucklescript-addons).

A concrete example would be putting EventEmitter:

module EventEmitter = struct
  type ('k, 'v) event = .. (* [@bs.string] *)
  type (_,_) event += …
  class type ['k] t = object
    method emit : ('k, 'v) event -> 'v -> 'k t Js.t
    method on : ('k, 'v) event -> ('v -> unit) -> 'k t Js.t
    method once : ('k, 'v) event -> ('v -> unit) -> 'k t Js.t
  end [@bs]
end

… in some bucklescript-node library, and then being able to reuse that class type in another library bucklescript-electron where I define App which inherits from EventEmitter.t:

module App = struct
  type ns = [`App | `EventEmitter]
  type (_,_) EventEmitter.event += …
  class type t = object
    inherit [ns] EventEmitter.t
  end [@bs]
end

So I guess there are really a few subtle points here:

  1. How to specify the variants (which should get translated to strings) separately from the external or method, so they can be reused (e.g., emit, on, and once should all support the same set of events).
  2. How to support translating variant constructors to strings in a way that can work with method instead of external, so that it can be used in the OO style with inheritance.
  3. How to support GADT constructors as strings, since this seems necessary to fit some of the method signatures within OCaml's type system in a nice way. Essentially, we need to be able to attach special type information to string arguments so they can constrain other method arguments. The first example for this is supporting the methods emit, on, and once but there are others.

For instance, in flow you can say something like this:

interface Foo {
  bar(arg0: number, arg1: 'a', arg2: string): number;
  bar(arg0: number, arg1: 'b', arg2: number): string;
}

Using GADT constructors for 'a' and 'b', this can still be encoded without too much trouble even though OCaml does not have intersection types. But without GADTs, it would be hard to do in a clean way I think.

Longer term, it would be great if we could also specify that certain GADT variant constructors should be translated to specific booleans or numbers too. This would let us emulate the same kind of string and number refinement you can do with flow and typescript. There are several examples here where that's necessary to provide good typings.

bobzhang commented 8 years ago

From what I understood of your first comment, bs.string only works with external so this couldn't be done as a method, is that correct? But in that case, you won't be able to put it in a class type that you can inherit from.

Correct, that's the current case, you have to fall back to string, where you loose some type safety.

How to support translating variant constructors to strings in a way that can work with method instead of external, so that it can be used in the OO style with inheritance.

It is quite tricky to give special treatment to FFI class type (doable in external), since OCaml is structural typing, the use site is decoupled from the def site, the only way to do this in a sane way is to give the data a uniform runtime representation (compile extensible variants into string in all places, see my comments later).

How to support GADT constructors as strings, since this seems necessary to fit some of the method signatures within OCaml's type system in a nice way.

We have such information in the typedtree IR. (see comments later)

you can model interface Foo faithfully in BuckleScript using external, (but you loose inheritance), in class type, the closest one is

class type foo = object
   method bar_number (* will be compiled to `bar`) : float -> string -> string -> number
   method bar_string : float -> string (* loose some type safety here *) -> number -> string
end [@bs]

So I understand your goal much better now, there are two kinds of FFI: external and class type (you can always mix them together),

Pros of class type FFI

So the long term solution is to customize OCaml's runtime representation better for JS runtime, currently, the BuckleScript works as below

-- reuse ocaml's parser -- > surface syntax  -- ast transformation for attributes -->
-- reuse ocaml's type checker --> typedtree -- reuse ocaml's pattern match compiler -->
--> ocaml's lambda IR --> BuckleScript's lambda IR --> BuckleScript's JS IR --> string

If we don't use OCaml's pattern match compiler, work from typedtree, we have all the information we need, and can customize runtime representation for JS runtime, actually, that's why we introduce our own lambda IR to make it future proof.

OCaml's pattern match compiler is not hard to swap out, and it makes sense to customize the pattern match compiler for JS backend, I am thinking of we can introduce BuckleScript's typedtree (for stable API) so that we can work from there.

ocaml's typedtreee --> bucklescript's typedtree -->
 bucklescript's lambda IR --> bucklescript's JS IR

So to conclude, it's doable and should be done in the long term, but we need deliver something first before bringing a better solution (and potentially more talented programmers than me).

For the short term goal, I think we can improve the polymorphic variant solution to make it available to BuckleScript users.

ghost commented 8 years ago

@bobzhang thanks so much for the thorough writeup. This helps me understand the current situation much better.

I'm already really happy with how well BuckleScript works but mostly thinking long term here, so if some of the issues around class type style FFI can be improved in the future as you suggest, I think that addresses my main concerns.

The idea to swap out the OCaml pattern compiler in order to be able to preserve type information further down the chain seems pretty reasonable. I would be willing to try and help with that but I need to understand the current code structure better first.

I think you've answered my original questions so I'll go ahead and close this.

alainfrisch commented 8 years ago

I fully agree that to match Bucklescript goals, working from the Typedtree seems the best long-term option. For instance, it would allow compiling sum types in a very different, much more JS-idiomatic way: an object with one discriminator string field, and other fields for the constructor arguments (and inline records give a way to specify names in the OCaml type definition). It will also allow compiling OCaml modules (including first class, etc) to JS objects, preserving field names. And much more.

Another advantage is that you probably already have all the information available in the Typedtree (whereas you'd need to maintain or upstream patches to Lambda).

It could even make sense to ship Bucklescript as a .cmt->.js translator, for straightforward integration in existing build systems, no?

bobzhang commented 8 years ago

@freebroccolo are you interested in the direction of enhancing the current polymorphic variant solution? If so, I can spend some time improving it per your feedback @alainfrisch Yes, as long as we have more support and resources : )

ghost commented 8 years ago

@bobzhang yes, I think that would definitely help. It would be great if you could do that :)

Risto-Stevcev commented 8 years ago

@bobzhang I'm currently running into a similar issue. Basically I have something like a settings object with an encoding property that can only be on a finite set of encodings, but the JS library that uses it needs to ingest it as a string. For example:

(* module Foo *)
type encodings  = [ `utf8 | `ascii | `base64 ];;
(* module Bar *)
type settings = {
  encoding : Foo.encodings;
  ...
};;
(* module Baz *)
(* unfortunately I have to duplicate the encodings type here *)
external baz : ([ `utf8 | `ascii | `base64 ] [@bs.string]) -> string = "" [@@bs.module "qux"];;

A solution you mentioned would fix this issue:

I am thinking that actually we can make this work

type event = [ `quit of (Event.t -> int -> unit [@bs]) ] [@@bs.string]

external on : t Js.t -> (event [@bs.string]) -> unit = "" [@@bs.send]
external once : t Js.t ->  (event [@bs.string]) -> unit = "" [@@bs.send]

So then I could just do:

(* module Baz *)
external baz : (Foo.encodings [@bs.string]) -> string = "" [@@bs.module "qux"];;
bobzhang commented 8 years ago

@Risto-Stevcev I did not prioritize this since such syntactic sugar is not very solid, it work in most daily cases, but maybe cause some surprise in some cases. Are you aware this discussion #686 , it may cover your need by using GADT, currently the output is not as good as we have now, but once we improve the inliner, so it will catch up eventually. Another more solid syntax sugar would be something like:

include 
struct external read : 'a t -> 'a = "" 
 [@@bs.val]
end  [@@bs.constraint type 'a t = 
  | Structure : structure t 
  | Signature : signature t 
 ] (* this attribute will be inherited in all externals inside*)

But I collected some feedback that most people don't want too much syntactic sugar in the FFI, feel free to chime in if you think it matters, discussion #616

Risto-Stevcev commented 8 years ago

Ah, I see. GADTs might help, but I feel like it might be overkill for this use case :stuck_out_tongue: