calmm-js / partial.lenses

Partial lenses is a comprehensive, high-performance optics library for JavaScript
MIT License
915 stars 36 forks source link

TypeScript and Flow type definitions #55

Open polytypic opened 7 years ago

polytypic commented 7 years ago

Would be nice to have TypeScript and Flow type definitions for partial lenses.

polytypic commented 7 years ago

@gcanti s related work:

gcanti commented 7 years ago

@polytypic FYI ts-static-land (an early attempt) is superseded by https://github.com/gcanti/fp-ts which goal would be to be compatible with both fantasy-land and static-land (hopefully!)

KiaraGrouwstra commented 7 years ago

I made a bit of progress on TS typings in my fork. Not that I imagine typing FP libs to be easy (I ended up as the maintainer of the Ramda typings, so know some of the challenges involved, some yet unresolved), and admittedly I'm still not as fluent in this library yet either. Then again, working on typings will hopefully help there. :)

KiaraGrouwstra commented 7 years ago

I'm getting the impression the main challenge here is just... TypeScript's type language can't handle the reduce type logic needed to type R.path, which means that typing this library for proper type inference becomes... problematic, since that kind of logic is the essential cornerstone here.

So monocle.ts has managed to keep things typed, but essentially at the cost of sacrificing expressivity for verbosity.

This makes me wonder: is there any way we could have our pie and eat it too, in any language (probably meaning Haskell/Purescript, if any at all)? I glanced over their lens libraries listed in the readme here for a bit, but I'm not so familiar with them, and haven't quite managed to tell if any enables expressive constructs as in this library while also being typed without getting a bunch more verbose.

Probably a bit off-topic here, but I'd be pretty curious!

gcanti commented 7 years ago

@tycho01 the first example in the README of monocle-ts can be reduced from

import { Lens, Optional } from 'monocle-ts'

const company = Lens.fromProp<Employee, 'company'>('company')
const address = Lens.fromProp<Company, 'address'>('address')
const street = Lens.fromProp<Address, 'street'>('street')
const name = Lens.fromProp<Street, 'name'>('name')

const lens = company
  .compose(address)
  .compose(street)
  .compose(name)

to something like this

import { Lens, Optional } from 'monocle-ts'

const lens2 = Lens.fromPath<Employee, 'company', 'address', 'street', 'name'>(['company', 'address', 'street', 'name'])

where

// more overloadings here...
function fromPath<T, K1 extends keyof T, K2 extends keyof T[K1], K3 extends keyof T[K1][K2], K4 extends keyof T[K1][K2][K3]>(path: [K1, K2, K3, K4]): Lens<T, T[K1][K2][K3][K4]>
function fromPath<T, K1 extends keyof T, K2 extends keyof T[K1], K3 extends keyof T[K1][K2]>(path: [K1, K2, K3]): Lens<T, T[K1][K2][K3]>
function fromPath<T, K1 extends keyof T, K2 extends keyof T[K1]>(path: [K1, K2]): Lens<T, T[K1][K2]>
function fromPath<T, K1 extends keyof T>(path: [K1]): Lens<T, T[K1]>
function fromPath(path: Array<any>) {
  const lens = Lens.fromProp<any, any>(path[0])
  return path.slice(1).reduce((acc, prop) => acc.compose(Lens.fromProp<any, any>(prop)), lens)
}

Perhaps the same technique can be used to add typings to R.path and / or this library?

KiaraGrouwstra commented 7 years ago

If sticking with strings as well (navigating structures of objects, no arrays), that works. When trying to extend this to numbers to navigate structures including tuples though, this breaks down.

// string, ok
declare function path<T, K1 extends keyof T>(path: [K1], v: T): T[K1]
let a = path(['a'], { a: 1 }) // number
// number, fails
declare function path<T, K1 extends number>(path: [K1], v: T): T[K1] // Type 'K1' cannot be used to index type 'T'.
let b = path([0], [1])

The R.path type has indeed used overloading to achieve a type signature that fell short of handling tuples, but for a path length n, this meant 2^n extra overloads, and predictably, at a certain number of overloads this just trashed performance to the point applications would no longer compile due to the addition of this type definition. And that's why I'm frustrated with TS for not dealing with type inference in a better (reduce-enabled) way, and am willing to turn to other languages if needed.

That said, for R.path forcing users to manually type results could be a solution (expected in/out types), but this sorta means the type definitions are failing their inference job.

From a quick glance of the current library, the situation would get more complex than just string vs. number though: arrays here could contain any optic (to be created using any of the dozens of functions in this lib or just string / number), and essentially you'd end up having to manually type most of it anyway... hence I'm now wondering if there's some Haskell version of this that could get inference right, or if Haskell and JS really just have their own respective strengths.

gcanti commented 7 years ago

fell short of handling tuples

FWIW you can see a tuple [A, B] as an object with '0' and '1' props though, so no need for K1 extends number

type A = {
  a: [number, [string, boolean]]
}

const lens3 = fromPath<A, 'a', '1', '0'>(['a', '1', '0'])

console.log(lens3.get({ a: [1, ['s', true]] })) // => 's'

AFAIK all solutions in typed languages are based on

KiaraGrouwstra commented 7 years ago

I did try that idea with inference, though that seemed to have turned out less well somehow:

declare function path<T, K1 extends keyof T>(path: [K1], v: T): T[K1]
let b = path(['0'], [1])
// intended result: 1
// actual result: error
// Argument of type '["0"]' is not assignable to parameter of type '["length" | "toString" | "toLocaleString" | "push" | "pop" | "concat" | "join" | "reverse" | "shi...'.
// Type '"0"' is not assignable to type '"length" | "toString" | "toLocaleString" | "push" | "pop" | "concat" | "join" | "reverse" | "shif...'.

That last example on the Monocle site looks pretty good. Just realized much of the folds from this library are also in ekmett/lens, among other things... I feel like this now.

gcanti commented 7 years ago

I guess [1] is inferred as Array<number> instead of [number], this works

declare function path<T, K1 extends keyof T>(path: [K1], v: T): T[K1]
let b = path(['0'], [1] as [number])
KiaraGrouwstra commented 7 years ago

Cool, thanks!

Trying path length 2:

declare function path<T, K1 extends keyof T, K2 extends keyof T[K1]>(path: [K1, K2], v: T): T[K1][K2]
let b2 = path(['a', '0'], { a: [1] })
// Argument of type '["a", string]' is not assignable to parameter of type '["a", never]'.
// Type 'string' is not assignable to type 'never'.
let b3 = path(['a', '0'], { a: [1] } as { a: [number] })
// Argument of type '["a", string]' is not assignable to parameter of type '["a", never]'.
// Type 'string' is not assignable to type 'never'.

Okay TypeScript, I don't think we were meant to work out...

KiaraGrouwstra commented 7 years ago

I made a proposal that'd enable the reduce-like logic needed for typing functions from this repo (as well as similar Ramda ones); currently not looking positive though...

KiaraGrouwstra commented 7 years ago

I've recently been exploring type recursion as a way to better address cases like R.path, which would also solve the basis for the current repo, see here. Typing this repo with working inference is still my main challenge for TS, a step up still from Ramda. That said, I think we only need two TS upgrades before we could do just about anything...

siassaj commented 5 years ago

@tycho01 Really interesting stuff, is there any new news about typings?

KiaraGrouwstra commented 5 years ago

@siassaj TS can operate on anything other than generic function types now, given Concat and pattern matching using conditional types. so while R.path may have progressed a bit, for this library I'm still not sure if there's much hope yet...

siassaj commented 5 years ago

thank you so much for the instant response.

It's a shame, I've run into some typing limitations in my own work too... the really higher order stuff gets so complex so fast...

I'm going to inspect your fork and see if I can't brute force some typings as needed to get most of the safety I need.

KiaraGrouwstra commented 5 years ago

@siassaj if you come up with something nice let us know! :dagger:

rjhilgefort commented 4 years ago

I have taken a look at monocle-ts as well as shades and nothing comes close to this library. Has anyone found a good, typed alternative to partial.lenses?

polytypic commented 4 years ago

Have you looked at optics-ts? It is a new optics library being developed specifically for TypeScript by @akheron.

As another possibility, I recently came up with a new (to me) approach to optics while working in a project developed in C# and made a public repo in F# to demonstrate the approach: NetOptics. The significance of the approach is that it doesn't require an encoding of higher-kinded types, ad hoc polymorphism, or even the use of interfaces to manipulate (higher-kinded) abstractions and yields a relatively concise and efficient implementation. Of course, the approach has its limitations (no (higher-kinded) applicative traversal), but if one is using a language (like TypeScript) that doesn't support higher-kinded types, then the more limited and simpler typings might be a good trade-off (C# 7.3, in particular, doesn't even provide parameterized type aliases, so dealing with complex parameterized types in C# would just be incredibly cumbersome). I have no plans to make a TypeScript library using the approach, but if someone is interested in giving it a try, I can probably help to get started.

rjhilgefort commented 4 years ago

Thank you for the reply @polytypic! I'm going to check out if optics-ts is a good enough substitute and then might approach you for the latter idea if needed. Cheers!

akheron commented 4 years ago

optics-ts is still pretty new, but the type construction it uses seems to allow typing many optics that other TypeScript libraries cannot type properly. Don't hesitate to create issues or pull requests for missing functionality!