typescriptfunctional-programminglensesmonocle-scala

Functional Programming/Optic concept that takes a partial object and returns a "filled in" object using lenses and traversals?


(Edit I'm using monocle-ts, but if it's not possible with monocle-ts (since the author even says it's just a partial port of the original Monocle for Scala) but if there is something in another optics package for any language, I'm open to porting those ideas to TypeScript.)

Suppose I have a helper type Partial<A> such that it represents a record that has some or all, but no non-members, of type A. (So if A = { foo: number, bar: string } then Partial<A> = { foo?: number, bar?: string }) (Edit This is Typescript's built-in Partial utility type.)

I begin with

interface Parent {
  xs: Child[]
}

interface PartialParent {
  partialxs: Partial<Child>[]
}

declare function fillInTheGaps(x: Partial<Child>):Child

Suppose I have composed a lens and traversal composed (composedTraversal) so that it focuses on partialxs from PartialState and then traverses over it as an array. This would be a Traversal<PartialState, Partial<Child>>.

Posit also that I have a declare const fn = (x:Partial<Child>):Partial<Child> then I can apply fn to all children with composedTraversal.modify(fn)(partialState) which will yield a new PartialState with fn applied to all of the partialxs.

Is there some concept that lets me "broaden" or "transform" this traversal into something different so that I could compose the lens and traversal and use fillInTheGaps so I can pass in the PartialState and get back a State?

Ignoring that my syntax is TypeScript, I've added the monocle-scala tag because if this concept exists I imagine it's in the Monocle library and I can translate that knowledge to the library I'm using.

Edit The problem motivating this question is I have a form input in a Redux app where a user inputs data but most is not required. The INPUTs are not known at compile-time (they're retried from a RESTful API query) so I cannot represent the model as

interface Model {
  foo?: string[]
  bar?: string[]
}

Instead, it is represented as

interface Model {
  [index:string]: string[]
}

I can also fetch a default model from the RESTful server. So I've modeled these as Parent (what comes from the server) and Partial<Parent> (what represents the user's input in the app).

Before doing some computations, I need to fold in defaults for the missing props. This is my fillInTheGaps function referenced above.

The desire was to enforce what this does via types in my code and, because I have a lot of optics already written, reuse some of that. I actually have a lens and traversal written to perform other operations on this data. myLens.compose(myTraversal).modify(fn) takes a Partial<State> and returns a Partial<State> but I was hoping to compose these to end up with a function that takes the partial and returns the whole.

I could obviously just write const filler: (Partial<State>):State = myLens.compose(myTraversal).modify(fillInTheGaps) and then throw a //@ts-ignore above it and know it would work, but that seems, uh, fragile.


Solution

  • I think, what you might want is a Polymorphic Traversal or PTraversal<S, T, A, B>.

    A Traversal<S, A> says, "If I have a function A => A, I can use modify to obtain a function S => S that uses the original function to modify all of the As that appear in S".

    By comparison, a PTraversal<S, T, A, B> says, "if I have a function A => B, I can use modify to obtain a function S => T", this converts all of the As in S to B, producing a T.

    Mnemonically, the type parameters of PTraversal are:

    PTraversals are useful, because they let you write things such as the following:

    PTraversal<Array<A>, Array<B>, A, B>
    

    Letting you traverse over an Array while changing the type of the elements.


    In your specific case, you've mentioned having two functions:

    declare function fillInTheGaps(x: Partial<Child>):Child
    declare function fn(x: Partial<Child>):Partial<Child>
    

    These can be composed together to produce a single function:

    function transformAndFill(x: Partial<Child>): Child {
       return fillInTheGaps(fn(x)); 
    }
    

    You'll then need to write a PTraversal<PartialState, State, Partial<Child>, Child>.

    This would support composing with a Lens to make a new PTraversal in much the same way that Traversal did.

    This should be doable, I think from your question that if you can convert every Partial<Child> in PartialState to a Child you should be able to make a State.


    PTraversal exists in Monocle (the Scala library), but unfortunately it doesn't look like it's made it into monocle-ts: So you would unfortunately have to write quite a lot of optics library code in order to support this.