Skip to content
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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Naming convention for qualified imports in the fp-ts eco system #1415

Open
m-bock opened this issue Mar 1, 2021 · 4 comments
Open

Naming convention for qualified imports in the fp-ts eco system #1415

m-bock opened this issue Mar 1, 2021 · 4 comments

Comments

@m-bock
Copy link
Contributor

m-bock commented Mar 1, 2021

I stumbled across this several times and maybe it has been discussed as well. How to name qualified imports in the fp-ts ecosystem?

Situation in PureScript/Haskell

In languages like Haskell or PureScript qualified import aliases don't share the same namespace as types.

So e.g. in PureScript you can import and use the Either module and type like this:

module Main where

import Data.Either as Either
import Data.Either (Either)

foo :: Either String String
foo = Either.Left ""

Limitation in TypeScript

In TypeScript the import is just a plain object and theoretically it should not conflict with the type Either, even if the new import type is used:

import type { Either } from 'fp-ts/Either';
import * as Either from 'fp-ts/Either';

This produces as TS2300: Duplicate identifier 'Either'.

I could not find reason for this, as this works with no complains:

type T = string
const T = 12

Proposals, Ideas

  1. Technically the following workaround works. But it's not so convenient as one has to repeat all the type arguments.
import * as Either from 'fp-ts/Either';

type Either<A, B> = Either.Either<A, B>;
  1. In most code I see capital character abbreviations used for module aliases, like: import * as E from 'fp-ts/Either'. I really think that this is confusing and does not scale. Suddenly you have another type that starts with "E" and many questions arise.

  2. I started to use the lower case alternative for a while now: import * as either from "fp-ts/Either"
    Traditionally JS modules used to be named lower case (fs, child_process) but since ES6 this seems to be replaced with an upper case convention (import * as React, ...)
    Also it's a bit silly that now one has to write either.either to reference the instance record.

  3. Another approach was to postfix the modules Either_ or Either$, but that also a bit ugly. or prefix $Either, _Either, I don't know.

  4. fpEither, fptsEither fpTsEither, FpTsEither, FpTsEither ... The downside of this is that you'll have a lot of "fp"s in the code, but on the other hand it may be useful for newcomers to see which modules follow the fp-ts patterns and which not.

Further reading: http://blog.haskell-exists.com/yuras/posts/namespaces-modules-qualified-imports-and-a-constant-pain.html

@mikearnaldi
Copy link
Contributor

I don't think point 2 is correct, "scale" is used without context.

Generally speaking, any rule you want to enforce for everything will be painful most of the time (like the verbosity of using Either.Either, Either.chain, and similar).

It is correct that sometimes you find yourself with 2 (or more types) that start with E in the same module but you can either reduce the size of the module and split things or just have local alias, for example, many times I find myself using Either and Exit (a different module) in the same file but Exit is used in 3 places and Either in 100 so the aliasing E = Either & Ex = Exit maximize the productivity locally.

The only pattern that can "scale" is: "do the thing that works best for your problem, don't try to find a rule that covers everything"

@kolyamba2105
Copy link

kolyamba2105 commented Mar 1, 2021

AFAIK you can achieve similar behavior like you showed in PureScript example in TS:

import * as E from "fp-ts/Either"

type Either = E.Either

[UPDATE] Invalid syntax above. That's a valid one:

import * as E from "fp-ts/Either"

import Either = E.Either

IMO it's pretty convenient to import everything just from fp-ts:

import { either, option } from "fp-ts"

You would still have to write either.either though, but it's not a big deal IMO 🙂

@gcanti
Copy link
Owner

gcanti commented Mar 2, 2021

This is invalid syntax:

import * as E from 'fp-ts/Either'

type Either = E.Either // Generic type 'Either' requires 2 type argument(s)

However this is valid:

import * as E from 'fp-ts/Either'

import Either = E.Either

The following style is fine for tree shakeability:

import { either } from 'fp-ts'
import { Either } from 'fp-ts/Either'

const foo: Either<string, string> = either.left('')

as long as you are using rollup or webpack 5.

You would still have to write either.either though

Just a note: "mega" instances like either.either are deprecated (in v2.10) in favour of small, specific instances, so for example:

import { either, option } from 'fp-ts'

// deprecated
const sequence = option.sequence(either.either)

// ok
const sequence = option.sequence(either.Applicative)

@m-bock
Copy link
Contributor Author

m-bock commented Mar 2, 2021

@mikearnaldi you're right. i was not specific enough about "scaling". And indeed, I agree, in the end it counts what works best for your problem. However, what you describe with "Exit" and "Either" is want I want to avoid.

Of course it's not about to find one golden rule, just to sort out different approaches and their implications.

Personally the either.either is not so bad, especially as @gcanti pointed out that in future smaller particular instances will be the norm, it will be obsolete for large parts anyways.

import { either } from 'fp-ts'
import { Either } from 'fp-ts/Either'

The above looks like a good way to go then, if you opt for verbose aliases.

One complaint I hear a lot from people starting with functional programming: There are so many imports! An in deed, it's a bit annoying.

so, why not:?

import * as fp from "fp-ts"

As far as I see, according to this document, it should be tree-shaking save, too.

Maybe fp.either.Either is a bit too much then, but how would it be, if the index.ts module exports the a major type for each module? (e.g. with export {Either} from "./Either") Then you could do:

import * as fp from "fp-ts"
import { Either, Option, State } from "fp-ts"

Then you'd have only two import lines, the types not qualified, which is convenient and to me writing things like fp.option.chain is not a problem. If you're ok with fp.Either then you even just have one import line instead of typically 10 or so.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants