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鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
Some struct
Functions
#1460
Comments
Copy-paste from functional programming slack. I would also like to see this module. It would be nice if it had functions like setAt, getAt, insertAt, mapAt, removeAt, createAt. me and @r-cyr had a discussion about this at some point. I'll post a snippet of functions and their signatures that we came up with. Looking at the initial post snippet, there are some methods that already cover some of these. e.g. evolve is really nice and covers mapAt.We have also discussed issue similar to unsafety of singleton function for the case of setAt (since you can say some very rough code that i'm not even sure works, but has some ideas function setAt<T, Key extends keyof T>(key: Key, value: T[Key]) {
return (t: T): T => ({ ...t, [key]: value })
}
function insertAt<Key extends string, V>(key: EnsureLiteral<Key>, value: V) {
return <T extends object>(
t: EnsurePropertyNotExist<T, Key>
): T & { readonly [K in Key]: V } => {
return {
...t,
[key]: value
} as any
}
}
export function createAt<T, K extends string, A>(
key: K extends keyof T ? never : K,
fn: (obj: T) => A
) {
return (obj: T): T & { readonly [k in K]: A } => ({ ...obj, [key]: fn(obj) });
}
export function replaceAt<K extends string, V>(key: K, value: V) {
return <T extends { [k in K]: unknown }>(obj: T): Omit<T, K> & { readonly [k in K]: V } => ({ ...obj, [key]: value });
}
export function removeAt<K extends string>(key: K) {
return <T extends { [k in K]: unknown }>({ [key]: _, ...rest }: T): Omit<T, K> => rest;
} |
This is wonderful! I have some ideas:
Thoughts? |
Just seen this - happy to rename |
I'm not sure if @qlonik has any input, but my 2 cents is to go ahead w/ |
I think so too if As for other points:
Possibly? Those two functions are a little different - one is preserving incoming object type
No. I just didn't go through the proposal very thoroughly 馃槃. Looking at omit in fp-ts-std, I don't like usage of
I'm all for using better names than those initial |
Re: It's possible to do both, though it doesn't always work when a function parameter expects an Re: I actually like the Re: names lodash has a set, ramda has a set. I'm leaning towards Btw I noticed that neither lodash nor ramda have something like an |
Re: Oh my mistake - I just looked at the implementation of (Edit: figured it out) |
Re: I too was going through docs to figure out the performance issues of Looks like we could use delete, but there might be penalty. Lodash too uses delete in this case, it seems. Alternatively, we could copy the entire object, while omitting certain properties specified in the filter array. I would also note that in the link, reduce and spread produces a lot of copies of the object. It would be better to implement it as for loop. This way the object is only allocated once and properties would be copied into it. Re: names
Re: I find that it would be nice to merge them, but it seems that when we are trying to merge, the complexity of types increases significantly. To me, keeping them separate is somewhat like the existence of EDIT: omit with for loop |
Re: Oh that's interesting. It sounds like you're describing this kind of thing - does this look right? const Do = {}
const a: { b: number; c: string } = pipe(
Do,
let('b', 3),
let('c', 'xyz'),
bind('d', ({b}) => b+ 1),
) In that case, is there a use case where that's easier/simpler than this? const a = pipe(
{},
(obj) => ({ ...obj, b: 3 }),
(obj) => ({ ...obj, c: 'xyz' }),
(obj) => ({ ...obj, b: obj.b + 1 })
) Re: I agree - though I'm still concerned that I think we're on the same page about (Edit: updated examples) |
Re: Yep, it is similar to the arrow function example. I find point-free style simpler and it removes some of the plumbing with respect to returning an object, not forgetting to copy other props and such. Re: It is similar to the above, where it provides a nice point-free function. It has a difference between |
Ok - I'm convinced! I added everything to the spec above and updated the rough draft. I went w/ I believe the ball's in @gcanti 's court now - ofc no pressure and no rush. Maybe this is a couple (or a few?) PRs? |
I can transfer this issue there if you want to. As a general observation I would try to come up with a few orthogonal APIs ( First of all (just as a base line a without egonomic concerns) let's see what we can do now with the existing APIs:
do notation ( import * as I from 'fp-ts/Identity'
const evolveOut = pipe({ a: 'a', b: 1, c: 'abc' }, ({ a, b, ...rest }) =>
pipe(
rest,
I.bind('a', () => a.length),
I.bind('b', () => b * 2)
)
)
assert.deepStrictEqual(evolveOut, { a: 1, b: 2, c: 'abc' })
const pickOut = pipe({ a: 1, b: 'two', c: [true] }, ({ a, c }) => ({ a, c }))
assert.deepStrictEqual(pickOut, { a: 1, c: [true] })
const omitOut = pipe({ a: 1, b: 'two', c: [true] }, ({ b, ...rest }) => rest)
assert.deepStrictEqual(omitOut, { a: 1, c: [true] })
When guards will be implemented: import * as O from 'fp-ts/Option'
const guard = (p: boolean): O.Option<void> => (p ? O.some(undefined) : O.none)
const refineOut1 = pipe(
O.some({ a: 'a', b: 1, c: 'abc' }),
O.chainFirst((x) => guard(x.a === 'a' && x.b === 1))
)
assert.deepStrictEqual(refineOut1, O.some({ a: 'a', b: 1, c: 'abc' }))
const refineOut2 = pipe(
O.some({ a: 'a', b: 2, c: 'abc' }),
O.chainFirst((x) => guard(x.a === 'a' && x.b === 1))
)
assert.deepStrictEqual(refineOut2, O.none) Note that you are not restricted to the import * as E from 'fp-ts/Either'
const guardEither = <E>(e: () => E) => (p: boolean): E.Either<E, void> =>
p ? E.right(undefined) : E.left(e())
const refineOut3 = pipe(
E.right({ a: 'a', b: 2, c: 'abc' }),
E.chainFirst((x) => guardEither(() => 'error a')(x.a === 'a')),
E.chainFirst((x) => guardEither(() => 'error b')(x.b === 1))
)
assert.deepStrictEqual(refineOut3, E.left('error b'))
do notation (Either monad): const parseOut1 = pipe({ a: 'a', b: 1, c: 'abc' }, ({ a, b, ...rest }) =>
pipe(
E.right(rest),
E.bind('a', () => (a === 'a' ? E.right(1) : E.left(`Not 'a'`))),
E.bindW('b', () => (b === 1 ? E.right('a') : E.left(42)))
)
)
assert.deepStrictEqual(parseOut1, E.right({ a: 1, b: 'a', c: 'abc' }))
const parseOut2 = pipe({ a: 'b', b: 1, c: 'abc' }, ({ a, b, ...rest }) =>
pipe(
E.right(rest),
E.bind('a', () => (a === 'a' ? E.right(1) : E.left(`Not 'a'`))),
E.bindW('b', () => (b === 1 ? E.right('a') : E.left(42)))
)
)
assert.deepStrictEqual(parseOut2, E.left(`Not 'a'`))
do notation: const traverseSOut = pipe(
O.some({ a: 1, b: 'b', c: 'abc' }),
O.chain(({ a, b, ...rest }) =>
pipe(
O.some(rest),
O.bind('a', () => (a <= 2 ? O.some(a.toString() + b) : O.none)),
O.bind('b', () => (b.length <= 2 ? O.some(b.length) : O.none))
)
)
)
assert.deepStrictEqual(traverseSOut, O.some({ a: '1b', b: 1, c: 'abc' }))
The signatures look a bit weird to me
do notation ( const doOut = pipe(
3,
I.bindTo('a'),
I.bind('b', () => false),
I.bind('c', ({ a }) => a.toString())
)
assert.deepStrictEqual(doOut, { a: 3, b: false, c: '3' })
do notation ( const setOut = pipe({ a: 1, b: 'a' }, ({ a, ...rest }) =>
pipe(
rest,
I.bind('a', () => 'a')
)
)
assert.deepStrictEqual(setOut, { a: 'a', b: 'a' }) Also this looks like a special case of p.s.
|
Wow this is all wonderful - what a great resource, thanks for doing all that! Hmm I will think about how to condense these into separate apis - out of curiosity, do you think that
Do you have some way to migrate this issue & all of its comments? That'd be great! I think this belongs there at this point
Are you saying there's some benefit to the current restriction requiring transformations for all keys? I'm also realizing that this one should maybe just be it's own issue on 'fp-ts', since
Stoked about guards! Is there some way to narrow the type of the output? (Though maybe that's part of what you meant by 'ergonomics aside')
Is there some way to do this that collects all the errors, instead of short circuiting?
Same question as above - could we collect them (w/ a Validation applicative, for instance)?
Could you be more specific? Do they look incorrect, dangerous, or maybe not useful?
This looks like a bit of a killer to me - @qlonik unless you disagree I don't think I'll move forward w/ these few functions, it looks more or less the same as our implementations Maybe there ought to be a separate issue upgrading all
I suppose these ones do really come down to ergonomics. I still think they're useful, but worth noting. If we're switching to
I see your point - even from their names. These don't quite seem to fit in |
There's a "Transfer issue" feature in GitHub, should migrate everything.
I think that your idea to allow properties to be omitted is good:
Ah I see... more like a export type Employee<P extends string> = {
readonly userId: string
readonly age: number
readonly position: P
}
declare const employees: ReadonlyArray<Employee<string>>
// v-- how to build this, right?
declare const isManager: (e: Employee<string>) => e is Employee<'Manager'>
export const seniorManagement: ReadonlyArray<Employee<'Manager'>> = pipe(
employees,
RA.filter(isManager)
)
You need a suitable import * as assert from 'assert'
import { pipe } from '../src/function'
import * as E from '../src/Either'
import * as S from '../src/string'
import { intercalate } from '../src/Semigroup'
import { apS } from '../src/Apply'
const myApS = apS(
E.getApplicativeValidation(pipe(S.Semigroup, intercalate(' | ')))
)
const parseOut1 = pipe({ a: 'b', b: 2, c: 'abc' }, ({ a, b, ...rest }) =>
pipe(
E.right(rest),
myApS('a', a === 'a' ? E.right(1) : E.left(`not 'a'`)),
myApS('b', b === 1 ? E.right('a') : E.left('not 1'))
)
)
assert.deepStrictEqual(parseOut1, E.left(`not 'a' | not 1`))
Yeah sorry.
type T = { a: string; b?: number }
declare const t: T
const x = unCompact(t)
/*
const x: {
a: Option<string>;
b?: Option<number> | undefined;
}
*/ the type of the field
/*
const x: {
foo?: number | undefined;
baz?: string | undefined;
} & {
bar: number;
}
*/
const x = compactS({
foo: O.some(123),
bar: 22,
baz: O.some('abc')
}) The result is ok but is not configurable. What if I want to encode That's why I think that they are more related to a decoding / encoding library
With a library like |
CRUD operations if we had import { constant, pipe } from '../src/function'
import * as I from '../src/Identity'
// the following signatures are temporary...
export declare const prop: <A, K extends keyof A>(k: K) => (a: A) => A[K]
export declare const pick: <A, K extends keyof A>(
ks: ReadonlyArray<K>
) => (a: A) => Pick<A, K>
export declare const omit: <A, K extends keyof A>(
ks: ReadonlyArray<K>
) => (a: A) => Omit<A, K>
export declare const evolve: <
A,
T extends { [K in keyof A]?: (a: A[K]) => unknown }
>(
transformations: T
) => (
a: A
) => {
readonly [K in keyof A]: T[K] extends (a: A[K]) => unknown
? ReturnType<T[K]>
: A[K]
}
//
// CRUD
//
const input = { a: 'a', b: 1, c: true }
// Create
// v-- without context
export const create1 = pipe(input, I.apS('d', new Date()))
export const create2 = pipe(
input,
// v-- with context
I.bind('d', ({ b }) => new Date(b))
)
// Read
export const read1 = pipe(input, prop('a'))
// Modify / Update
// v-- same type
export const modify1 = pipe(input, evolve({ a: (s) => s + '!' }))
// v-- different type
export const modify2 = pipe(input, evolve({ a: (s) => s.length }))
// v-- same type
export const update1 = pipe(input, evolve({ a: constant('s') }))
// v-- different type
export const update2 = pipe(input, evolve({ a: constant(1) }))
// Delete
export const delete1 = pipe(input, omit(['a']))
export const delete2 = pipe(input, pick(['b', 'c']))
// what if we want to change a key? (as a base line) same CRUD operations using import { identity, pipe } from '../src/function'
const readonly: <A>(a: A) => Readonly<A> = identity
//
// CRUD
//
const input = { a: 'a', b: 1, c: true }
// Create
export const create2 = pipe(input, (input) =>
readonly({
...input,
d: new Date(input.b)
})
)
// Read
export const read2 = input.a
export const read3 = pipe(input, (_) => _.a)
// Modify / Update
export const modify1 = pipe(input, ({ a, ...rest }) =>
readonly({ a: a + '!', ...rest })
)
export const modify2 = pipe(input, ({ a, ...rest }) =>
readonly({ a: a.length, ...rest })
)
export const update1 = pipe(input, ({ a, ...rest }) =>
readonly({ a: 's', ...rest })
)
export const update2 = pipe(input, ({ a, ...rest }) =>
readonly({ a: 1, ...rest })
)
// Delete
export const delete1 = pipe(input, ({ a, ...rest }) => readonly(rest))
export const delete2 = pipe(input, ({ b, c }) => readonly({ b, c }))
//
// Update key
//
export const modifyKey1 = pipe(input, ({ a, ...rest }) => ({
d: new Date(),
...rest
})) |
To recap: Atomic operations
In general we are looking for a way to build a function
for some effect I think I found an universal pattern for all these operations that depends on The pattern when import { pipe } from 'fp-ts/function'
import * as I from 'fp-ts/Identity'
export declare const pick: <A, K extends keyof A>(
...ks: readonly [K, ...ReadonlyArray<K>]
) => (a: A) => Pick<A, K>
export declare const omit: <A, K extends keyof A>(
...ks: readonly [K, ...ReadonlyArray<K>]
) => (a: A) => Omit<A, K>
type Input = {
readonly a: string | number
readonly b: number
readonly c: boolean
}
// universal pattern
export const transformationIdentity = (i: Input) =>
pipe(
i,
omit('a', 'b'), // <= use `omit` (or `pick`) to remove unwanted keys or mapped keys/values
I.apS('bb', i.b), // <= map keys
I.apS('a', String(i.a)), // <= map values
I.bind('d', ({ a }) => new Date(i.b + a)) // <= insert keys (I can possibly read from previous values using `bind`)
)
/*
const transformationIdentity: (i: Input) => {
readonly a: string;
readonly c: boolean;
readonly bb: number;
readonly d: Date;
}
*/ The pattern when import * as O from 'fp-ts/Option'
export const transformationOption = (i: Input) =>
pipe(
i,
omit('a'),
O.some, // <= lift to the desired effect, `Option` in this case
O.apS('a', typeof i.a === 'string' ? O.some(i.a) : O.none) // may fail
)
/*
const transformationOption: (i: Input) => O.Option<{
readonly a: string; // <= refined
readonly b: number;
readonly c: boolean;
}>
*/ @anthonyjoeseph note that from import * as RA from 'fp-ts/ReadonlyArray'
const myRefinement = O.getRefinement(transformationOption)
/*
const myRefinement: Refinement<Input, {
readonly a: string; // <= refined
readonly b: number;
readonly c: boolean;
}>
*/
declare const inputs: ReadonlyArray<Input>
const refined = pipe(inputs, RA.filter(myRefinement)) However there's no need to pass through a refinement if we are filtering as we can conveniently use const refined = pipe(inputs, RA.filterMap(transformationOption)) |
馃く this is amazing! Definitely could replace a Is there any reason that (Edit: I suppose |
Here's the same operations using 5 primitives: import { Endomorphism, pipe } from 'fp-ts/function'
import { Functor1 } from 'fp-ts/Functor'
import { Kind, URIS } from 'fp-ts/HKT'
import * as I from 'fp-ts/Identity'
import * as O from 'fp-ts/Option'
// -------------------------------------------------------------------------------------
// primitives
// -------------------------------------------------------------------------------------
export declare const pick: <S, P extends keyof S>(
...props: readonly [P, P, ...ReadonlyArray<P>]
) => (s: S) => Pick<S, P>
export declare const omit: <S, P extends keyof S>(...props: readonly [P, ...ReadonlyArray<P>]) => (s: S) => Omit<S, P>
export declare const insertAt: <S, P extends string, B>(
prop: Exclude<P, keyof S>,
value: B
) => (
s: S
) => {
readonly [K in keyof S | P]: K extends keyof S ? S[K] : B
}
export declare const rename: <S, F extends keyof S, T extends string>(
from: F,
to: Exclude<T, keyof S>
) => (
s: S
) => {
readonly [K in Exclude<keyof S, F> | T]: K extends keyof S ? S[K] : S[F]
}
export declare const mapAtE: <F extends URIS>(
F: Functor1<F>
) => <S, P extends keyof S, B>(
prop: P,
f: (sp: S[P]) => Kind<F, B>
) => (
s: S
) => Kind<
F,
{
readonly [K in keyof S]: K extends P ? B : S[K]
}
>
// -------------------------------------------------------------------------------------
// utils
// -------------------------------------------------------------------------------------
// derived from `mapAtE`
export declare const modifyAt: <S, P extends keyof S>(
prop: Exclude<P, keyof S>,
f: Endomorphism<S[P]>
) => Endomorphism<S>
// derived from `modifyAt`
export declare const updateAt: <S, P extends keyof S>(prop: Exclude<P, keyof S>, ap: S[P]) => Endomorphism<S>
// -------------------------------------------------------------------------------------
// derived
// -------------------------------------------------------------------------------------
const mapAt = mapAtE(I.Functor)
const mapAtO = mapAtE(O.Functor)
// -------------------------------------------------------------------------------------
// tests
// -------------------------------------------------------------------------------------
type Input = {
readonly a: string | number
readonly b: number
readonly c: boolean
}
export const transformationIdentity2 = (i: Input) =>
pipe(
i,
rename('b', 'bb'),
insertAt('d', new Date(i.b)),
mapAt('a', (a) => String(a))
)
export const transformationOption2 = (i: Input) =>
pipe(
i,
mapAtO('a', (a) => (typeof a === 'string' ? O.some(a) : O.none))
) |
I think I understand -
Is there a difference between |
If I'm not wrong (but please double check)
No, it's just an alias: declare const insertAt: <P extends string, S, B>(
prop: Exclude<P, keyof S>,
value: B
) => (
s: S
) => {
readonly [K in keyof S | P]: K extends keyof S ? S[K] : B
}
// excerpted from `Identity.ts`
declare const apS: <N extends string, A, B>(
name: Exclude<N, keyof A>,
fb: B
) => (fa: A) => { readonly [K in N | keyof A]: K extends keyof A ? A[K] : B } |
That looks like it covers everything to me! I don't understand the use of Out of curiosity, what does the If I understand correctly, |
Ofc this is very subjective, but what do you think of these names for the ergonomic functions instead? They're a bit shorter, and might be a bit more familiar:
|
@anthonyjoeseph I'd get rid of
For what concerns
Effect (another example: filterE i.e. a
|
Is there any way to work with structs that we don't quite know the shape of yet (generic)? For example, if I want to insert extra props into a structure for react component: import { flow, pipe } from 'fp-ts/function';
import * as I from 'fp-ts/Identity';
import { useState } from 'react';
export declare const insertAt: <S, P extends string, B>(
prop: Exclude<P, keyof S>,
value: B
) => (
s: S
) => {
readonly [K in keyof S | P]: K extends keyof S ? S[K] : B;
};
const x = <P extends Record<string, unknown>>(props: P) =>
pipe(props, insertAt('state', useState('hello')));
export const ComponentX = (props: { a: string; b: number }) =>
pipe(props, x, ({ a, b, state: [state, setState] }) => (
<div>
{a} {b}
<button onClick={() => setState('clicked')}>{state}</button>
</div>
));
const y = <P extends Record<string, unknown>>(props: P) =>
pipe(
props,
I.bind('state', () => useState(123))
);
export const ComponentY = (props: { a: string; b: number }) =>
pipe(props, y, ({ a, b, state: [state, setState] }) => (
<div>
{a} {b}
<button onClick={() => setState(123)}>{state}</button>
</div>
));
const z = flow(
I.bind('first', () => useState('hello')),
I.bind('second', () => useState(123))
);
export const ComponentZ = (props: { a: string; b: number }) =>
pipe(props, z, ({ a, b }) => (
<div>
{a} {b}
</div>
)); Interestingly the usage of functions |
I think in order for const z = flow(
I.Do,
I.bind('first', () => useState('hello')),
I.bind('second', () => useState(123))
); |
@cdimitroulas I think the trouble there is that @qlonik I think we could use a similar trick we use for As for changing
Not sure if any of those are in scope here. Feel free to open that stuff is as a separate issue if you'd like |
Oops, my bad. Serves me right for replying without testing my code 馃槀 |
Haha no worries, I can definitely relate! |
@gcanti I'm struggling a bit to understand the use case for export declare const mapAtE = <M extends URIS>(M: Chain1<M>) =>
<A, P extends keyof A, B>(prop: P, f: (ap: A[P]) => Kind<M, B>) =>
(a: Kind<M, A>): Kind<M, { readonly [K in keyof A]: K extends P ? B : A[K] }>
const mapAtO = mapAtE(O.Chain)
// you can map several keys on the same object
const a: O.Option<{ a: "abc"; b: 123 }> = pipe(
{a: 'abc', b: 123},
O.some,
mapAtO('a', (a) => a === 'abc' ? O.some(a as 'abc') : O.none),
mapAtO('b', (b) => b === 123 ? O.some(b as 123) : O.none)
) |
@anthonyjoeseph It's usual practice to use the weakest constraint possible, if the atomic operation is "modify a (single) key", If you need to modify multiple keys you can use a const mapAtO = mapAtE(O.Functor)
export const a: O.Option<{ readonly a: 'abc'; readonly b: 123 }> = pipe(
{ a: 'abc', b: 123 },
mapAtO('a', (a) => (a === 'abc' ? O.some(a as 'abc') : O.none)),
// v-- require a `Chain` instance only if you really need it
O.chain(mapAtO('b', (b) => (b === 123 ? O.some(b as 123) : O.none)))
) |
Since there's an overlap with functional optics, while we're at it I would like come up with consistent APIs:
For what concerns |
Follow up:
would be used in
would be specific to the |
That looks great! Just double checking - Also, I hate to ask for something so menial, but I'm having a bit of trouble implementing the |
I'm working on another possible solution to this - a fluent monocle-ts facade. (edit: it's on npm now!) It looks like this: import { set } from 'spectacles-ts'
const beenSet: { a: { b: number } } = pipe(
{ a: { b: 123 } },
set(['a', 'b'], -123)
) Here's the repo - it's called Maybe this kind of convenience code can live in that repo instead? P.S. To be clear - this is meant as a supplement to monocle-ts, not to replace it. It can only do around 5 operations before reaching the type instantiation limit (edit: updated repository name) |
While we're talking about the way struct functions behave, I have a gripe with the existing evolve with regards to partial structs. Here's a simplified example of the problem: interface A {
a?: 'a';
b?: 'b';
c?: 'c';
}
const a: A = {};
const b = F.pipe(
a,
evolve({
a: fromNullable,
b: fromNullable,
c: fromNullable,
})
); The type of b is not partial, however the actual value of b is |
@kalda341 great catch! I've submitted a separate issue for this bug |
closing in favor of gcanti/monocle-ts#183 |
馃殌 Feature request
Current Behavior
Desired Behavior
I have some ideas for new functions to go in the
struct
module that might be handy:Functions & Use cases
evolve
Already exists. My idea modifies it to allow properties to be omitted.
omit
&pick
Stolen from fp-ts-std - I love these functions and I think they might be a great fit here! They're great for omitting data at both type & value level
refine
Refines a struct based on some of its properties
parse
Parses an struct based on some of its properties
Might be good for heterogeneous html form data
traverseS
Might be likewise useful for forms that only want a single error message (i.e. combined w/ a
Validation
)I almost implemented a
refineMap
, but I realized thattraverseS
would be a more general solution.compactS
andunCompact
Useful for structures that contain heterogeneous optional values
Do
(let
,bindTo
,bind
)Pipe-able helpers to build an object. Point-free ergonomics that prevent mistakes & boilerplate
set
&setW
Typesafe port of lodash's set.
set
is especially helpful for creating genericEndomorphisms
(variousmodifyAts
,State.modify
etc.)Suggested Solution
Here are rough draft implementations
Who does this impact? Who is this for?
Developers who want to transform simple structed data w/ strong type safety
Describe alternatives you've considered
Maybe some of these could start off in fp-ts-contrib, like
sequenceS
did?Additional context
This started off as a PR (which was premature, since the features at the time weren't necessarily desired)
I'm not sure what to do about the
S
suffix ofcompactS
- theS
suffix is redundant since the function is already in thestruct
namespace, butcompact
doesn't seem quite right since its function signature doesn't fit insideCompactable.compact
.Your environment
The text was updated successfully, but these errors were encountered: