A better way to handle polymorphic function bindings

I’d like to double-check my understanding of binding to external functions with polymorphic values. I’m creating a new binding to a react component. What I’d like to do is something like this:

module CollapsedItem = {
  @module("react-native-paper") @scope("Drawer") @react.component
  external make: (
    ~label: string=?,
    ~badge: @unwrap  // This is what breaks
    [
      | #Str(string)
      | #Int(int)
      | #Float(float)
      | #Bool(bool)
    ]=?,
    ~focusedIcon: React.element=?,
    ~unfocusedIcon: React.element=?,
    ~active: bool=?,
    ~theme: Paper__ThemeProvider.Theme.t=?,
    ~onPress: ReactNative.Event.targetEvent => unit=?,
    ~accessibilityLabel: string=?,
    ~style: ReactNative.Style.t=?,
    ~theme: Paper__ThemeProvider.Theme.t=?,
    ~testID: string=?,
  ) => React.element = "CollapsedItem"
}

This breaks with the following error: @obj label badge does not support @unwrap arguments.

I’m aware that I could create something like the following (and I usually do):

module CollapsedItemWithStringBadge = {
  @module("react-native-paper") @scope("Drawer") @react.component
  external make: (
    ~label: string=?,
    ~badge: string=?, // Only handles a string
   ...
  ) => React.element = "CollapsedItem"
}

module CollapsedItemWithBoolBadge = {
  @module("react-native-paper") @scope("Drawer") @react.component
  external make: (
    ~label: string=?,
    ~badge: bool=?, // Only handles a bool
    ...
  ) => React.element = "CollapsedItem"
 
}
 ...etc

A glance at the docs and a search through this forum seems to confirm this is the standard approach.

This is fine for my usually limited use cases, but not ideal if the component has multiple overloaded properties and you’re trying to write flexible bindings for general use for everyone. i.e.- You don’t really want to write a make function for all permutations of prop options.

Would love to hear if there’s a better way!

1 Like

You could use a functor:

module Make = (T: { type t }) => {
  @module("react-native-paper") @scope("Drawer") @react.component
  external make: (~label: string=?, ~badge: T.t=?) => React.element =
    "CollapsedItem"
}

module StringItem = Make({ type t = string })
module IntItem = Make({ type t = int })

let items =
  <>
    <StringItem label="String" badge="ok" />
    <IntItem label="Int" badge=42 />
  </>

And you could use functors inside functors to generate permutations of different types. But IMO this sounds like such a messy API that I’d probably just create a wrapper API with a proper data model instead.

1 Like

Yep, that would work! It does result in potentially less LOC on my end, but still results in bindings where the user has to choose between components StringItem and IntItem, or in an expanded example: CollapsedItemWithStringBadgeAndIntThing, CollapsedItemWithStringBadgeAndStringThing, CollapsedItemWithIntBadgeAndIntThing, etc. … That said, thanks for chiming in, it’s prob an approach I need to consider playing around with.

There’s prob a good language/type-specific reason as to why this choice ultimately gets hoisted up to the component level, but it would be nice if there was a good way to handle it at the individual prop level, especially where individual props are optional (in my case they all are) and the overhead of creating separate components for various ones adds a lot of seemingly needless overhead.

You could use abstract type to represent all the different things you may want to pass in to the parameter. Then use that as the type for the parameter.

module Badge = {
  type t
  external s: string => t = "%identity"
  external i: int => t = "%identity"
  external f: float => t = "%identity"
  external b: bool => t = "%identity"
}

module CollapsedItem = {
  @module("react-native-paper") @scope("Drawer") @react.component
  external make: (~label: string=?, ~badge: Badge.t=?) => React.element =
    "CollapsedItem"
}

let y = <CollapsedItem badge={Badge.s("yo")} />
let z = <CollapsedItem badge={Badge.i(123)} />

(playground)

Edit: I just found a related question/answer here.

8 Likes

Ah, clever! I hadn’t thought about using multiple identity bindings like this. Also thanks for the link, not sure how I missed that one. Thanks!

1 Like