Should we add Namespaces to Haskell?

I’m really trying to read all your comments as considerate, nice, and constructive, but your confrontational writing style makes it hard sometimes. Remember that this is conversation about the language we both care about, Haskell :), not about winning an argument on the internet.

Your solution to the example is the standard Haskell approach, but it has some drawbacks (whether this warrants a new language extension is up for discussion).
First, it introduces a typeclass without any laws attached and with multiple solutions, many structures can be printed in different ways. Also pretty can not change type between the usages. Say I wanted to be able to colorize V3: pretty:: Color -> V3 -> Text, I would have to provide the same interface for V2, even if it is never used.
Second, I has to differentiate constructors. Mk2 and Mk3 this creates antipatterns
like newtype MyCoolType = MkMyCoolType { unMyCoolType :: Int }, which is clearly nicer when written newtype MyCoolType = Mk { un :: Int}.
Third this suffix anti pattern, makes it very hard to do good code suggestions in an editor.
Typing in Mk would report { Mk2, Mk3, ... MkMyCoolType}, typing in V3: would suggest {V3:Mk, V3:pretty} which is better.

It’s not and it does not have to :), my proposal is very incremental adding as few features as possible.

I’m not completely sure I get the problem. The goal of the proposal is that ultimately most imports are qualitated (or named), hopefully you would only include your prelude unqualified.

I really do not like using CPP unless I have to; It’s much better to use language features so the the syntax of the language captures the intent of the programmer. This also makes tooling a lot easier.

2 Likes

Thank you for taking a look :slight_smile: !

Using records as namespaces have three big disadvantages (though a cool idea). One, It relies on inlining to make your code performant, in-lining is sometimes hard to predict.
Two, it cannot namespace types. Three, it is a lot of syntactic noise to describe the same problem. One thing that is cool about the approach is that it allows for dynamic dispatching. You would essentially get the same effect with type classes:

class Vec p where
  type Dimension p
  mk :: Dimension p -> x
  pretty :: p -> String

However, this solution does not scale well with many functions in Vec or with very different arguments to mk. It also requires the code you create namespaces for to be related. Imagine that V2, has a flip operation that makes x = y and y = x. We can’t implement this for V3, but it still have to be part of Vec.

We’ll it simulates it, but it scales much better. Essentially the reason to allow namespaces to be types is actually in reverse, I want types to be namespaces.
In an ideal world (but not part of this proposal) you should be able to write something like:

data Point = Mk { x :: Float, y :: Float } 
 where
  -- patterns
  pattern Origon = Mk 0 0

  -- accessors
  norm :: Point -> Float
  norm = sqrt (x*x + y*y)

-- Now we can use the namespace created by Point
myPointPrinter :: Point -> String
myPointPrinter p = case p of 
  Point:Origon -> "O" 
  _ -> show (Point:norm (Point:Mk 0 2))

Arguable, this it mostly a question of preference and convenience. Reading that a type is local to a function enables you to mentally ignore it when reading a file. Also it enable you to name them correctly. For example your function f might need some intermediate collection of names, lets call it NameMap. To have it in our module we would have to call it NameMapF to see that it is different from NameMapG which is used in g, and is a little different. A more concrete example:

import Control.Monad.State named as State

space Counter = Counter (run, incr, Counter, Interface) where

  run :: Counter a -> (Int, x) 
  run m = runState (unwarp m) 0

  type Impl = State Int
  newtype Counter a = Impl { unwrap :: Impl a }
    deriving (Functor, Applicative, Monad) via Impl

  -- Abstract interface of Counter
  class Monad m => Interface m where
    -- increment the counter and get 
    incr :: m Int

  instance Interface Counter where
    incr (Impl s) = Impl (state (\x -> (x, x + 1)))

  open State

main = do 
  print $ Counter:run do
     let open Counter
     incr
     incr

Now I’m free to still use Impl and Interface other places in the same file without breaking previous code.

Thank you for looking through the proposal, and thank you for your feedback. I hope that I have convinced you, or at least made my position clear about the benefits of namespaces. I believe that namespaces would not only be an nice feature, but will fundamentally change the way we write Haskell code.

1 Like

yes, essentially I am making the type class dictionary explicit… :sweat_smile:

This looks like converting types in object without inheritance… but as you said this is not part of the proposal, so not worth debating

not really :grinning_face_with_smiling_eyes: but you made a good point, and I think is precisely why don’t like this proposal, because it changes the way Haskell is written. I am not saying it is perfect nor it shouldn’t evolve, but the point is

it doesn’t add any extra capability you can’t achive with current modules, but drastically changes the way haskell is written and works

Also, consider that all users are potentially force into such an extension. If you create a library using namespaces and I want to use it, I will need to use the namespace mechanisim even if I don’t like it, and this is very very different from other extensions.

If I want to use a library with complex types and all the fanfare, I am force to use extensions which enable new capabilities in the language; but if a library uses LambdaCase or any other syntactic-sugar extension, I am not force to use LambdaCase in my own code, because this extension do not add any extra capability. This proposal is the opposite, the ficticious NameSpaces extension would only add (sort of) syntactic-sugar for modules but users are force into such an extension in their own code.

True, but this is also true for many other libraries like DataKinds and GADTs.
I would guess that it would be made so that, if you are not using any namespace you don’t have to use Namespaces.

It would be up to the Library maintainers to export their namespaces in a way that is usable to others. For example, Hedgehog could write:

-- in Hedgehog.hs
module Hedgehog (forAll, Gen, Range) where
...
-- in Hedgehog/Gen.hs
module Hedgehog.Gen where
import Hedgehog (Gen)
open Gen
-- in Hedgehog/Range.hs
module Hedgehog.Range where
import Hedgehog (Range)
open Range

And thereby allow users to access the features of the library without using the Namespace syntax.

The point is that DataKinds or GADTs do add new capabilities for the type system: There are ADT you can not define without GATDs; same with DataKinds but NameSpaces do not add any new capability. All that can be achive with NameSpaces can be done with the current module system, with more files, submodules, etc…

Let say you use NameSpaces like in your example for a library:

module Pretty where

space V2 where
  data V2 = Mk { x :: Int, y :: Int }
  pretty :: V2 -> Text
  pretty = ...

space V3 where
  data V3 = Mk { x :: Int, y :: Int, z :: Int }
  pretty :: V3 -> Text
  pretty = ...

How would you make Pretty usable for someone how doesn’t want to use the extension? Notice that you use NameSpace only because you prefer the syntax, not because It provide something which I can’t do it without it . Whereas using the current module system you can write two modules with those functions. That’s the point!, a syntactic preference is imposed (too much of a word, but not a native speaker) to all the users which is different from DataKinds or GADTs which are needs: The type system behaves in a way that would have being impossible

It is reasonable that if you want to use a library with advanced type level features such that Haskell2010 doesn’t have, you need to enable extensions, but It doesn’t look right to do so for syntactic sugar, which is a matter of preference.

The ability to export namespaces is the only new feature; and if you do not use, it won’t affect any of your dependencies.

To gain the same effect, with more files; the library developer or the user of the library can create two files:

-- Pretty/V2.hs
{#- LANGUAGE Namespaces -#}
module Pretty.V2 where import Pretty (V2(..))
-- Pretty/V3.hs
{#- LANGUAGE Namespaces -#}
module Pretty.V3 where import Pretty (V3(..))
-- at use point, you'll use the old syntax.
import Pretty.V2 qualified as V2
import Pretty.V3 qualified as V3

main = print (V2.pretty (V2.Mk 0 1) <> V3.pretty (V3.Mk 0 1 0))

But I also want to point out that enabling the extension only affects how you write lists, you
have to add a space in front of 1:2:3 becomes 1 : 2 : 3. This won’t dramatically change the way we write haskell :).

I wasn’t focusing on the particular operator because that’s clearly not the point of the proposal :slight_smile: but I agree : would be a good choice.

But If a library author uses it, the users of such a library are now bounded to the extension.

The user of the library should not be responsable for author’s syntactic preferences… the same way author can use LambdaCase, BlockArguments, etc… but users don’t have to modify any code nor add any extension to use a library with only syntactic extensions on it.

I have never thought this much about how difficult is to evolve a language hahaha

1 Like

I just want to make sure that I make it clear. You only have to activate the language extension if you import and use a namespace, e.g. write open A or A:x in your code. So for a user of a library to be forced to enable the extension then.

  1. The library X uses the extension.
  2. The library exports functionally only in a namespace. They could choose to be backward compatible and use the trick from before and also export the feature through a module.
  3. The user needs import the library X AND actively access the namespace with open A or A:x. The fact that you import a namespace does not require the extension as it is parsed as a normal name import X is therefore fine.
  4. Then the user will be prompted to enable the extension.

No, and Haskell runs with a lot of baggage, which creates many interesting corner cases :D.

Ah, ok this sounds like a substantive topic for your ‘problem statement’. I would describe it as a prefix anti-pattern rather than suffix-ness ;-).

We’re only putting Mk... prefixes because the anti-punners want constructor names different to the type name. I don’t see that calling all data constructors bare Mk is going to help in program readability. And I strongly disagree with your Second point: not “clearly nicer”; I find it quite unidiomatic – like you’re trying to force some other language’s idioms on to Haskell.

The consequence is that everywhere you want to code a constructor, you need the namespace prefix. (It wasn’t obvious from the write-up prefixes are compulsory.) A recent discussion on another topic has caused me to doubt the wisdom of Mk... prefixes. Indeed now that I think of it, it’s equally unhelpful as Hungarian notation. Then …

  • Put the ...Mk as a suffix to the datatype name: V2Mk, V3Mk.
  • Now your editor’s code suggestions work nicely.
  • And I’ve saved a character over V2:Mk.
  • More important, all current uses of : (or :: or .) are unaffected.

Well not true, I’ll explain. But firstly: you challenged me to Example 1. You’ve now changed the rules. This is a different requirement. Which is why I want you to write down the requirements/problem statement before giving answers.

  • If you have two functions that result in prettified Text, but use different parameters to get there, those are different functions; don’t give them the same name, it’s again un-idiomatic (and confusing to readers of the code); and you’re merely abusing the namespace mechanism.
  • So I think you’ve decided to introduce namespaces into Haskell because Rust. Now you’re introducing bogus use cases in an attempt to justify your prejudices.
  • More news you didn’t disclose at the start of the exercise: Text can be colourised. So that means all functions producing Text must colourise it. What colour is the text from V2 to be, and how do I encode that?

Nevertheless, this is how you could do the Colour part (again not idiomatic Haskell – more a bodge because the user didn’t state the requirements clearly, and now you have to retrofit something to your widely-used class)

instance CPretty (Colour, V3) where pretty (c, V3Mk{ x, y, z }) = ...

main = print (pretty (V2Mk 0 1) <> pretty (Cyan, V3Mk 0 1 0)) 

If you try to call pretty (Magenta, (V2Mk 0 1)) or pretty (V3Mk 0 1 0), you’ll get a no instance rejection.

Or you could contrive pretty to be variadic, with the method’s type given by an Associated Type for the class. (Oleg Kiselyov was the pioneer of this sort of thing, back in the mid-2000’s and using only FunDeps. His code is a tour-de-force, and nearly always eye-wateringly hard to read.)

class CPretty a  where 
  type Tpretty a :: *
  pretty :: Tpretty a

instance CPretty V3  where
  type TPretty V3 = Colour -> V3 -> Text
  pretty c (V3Mk{ x, y, z}) = ...

(Almost certainly you’ll need a type annotation at call sites for pretty.)

[re using #include at the top of a module to get consistent imports across a whole project.]

Ok, so a variant of import to go at the top of a module – or even as part of the module syntax – that rather than importing Haskell source, imports a file that must contain only a bunch of import statements.

I think every industrial-sized Haskell library/application can’t avoid substantial amounts of cpp – because it needs fine-grained control over library versioning of imports, and versioning for the compiler. Then #includes are something everybody lives with.

1 Like

Yes, I realize now that was what you have asked for :), I have updated the draft proposal to contain a Why section:

I have also updated Example 1 to be more problematic :), the example with Pretty was a little to obvious to put in a type class; Instead I use a fromV2 as an example, of cause you can use the associated type trick as you state it yourself its not easy to read :).

Re the multiple small files (your option 1):

  • This was even more of a problem before there was -XDuplicateRecordFields, because the compiler complained about duplicate field names, even though you were accessing them in a non-ambiguous way.
  • I agree with the criticisms that 1 Compilation unit = 1 (head) module is too restrictive, especially because the module imports don’t distinguish those that are inside my application vs external Libraries/utilities.

Then … (can’t remember where I’ve seen this suggestion, I don’t think it was in the more formal proposal process) can we do less violence to the current module system:

  • Allow a file to contain multiple module decls.
  • The textually first is still the head, and controls what gets exported from this compilation unit.
  • Later module decls are treated as if they were separate files imported by the head.
  • The sub-modules’ entities are not exported from this compilation unit, unless the head module says so. IOW they’re effectively private namespaces.
  • The declared imports into the head module are also imported into the sub-modules, to give consistent naming through the application.

Your point 2 and revised Example 1 (continues to) have no force for me/makes me question whether you understand idiomatic Haskell.

  • The meaning/purpose of type classes is to capture same (not similar) typed functions. (It’s very unfortunate they’re called ‘methods’, because they bear only superficial similarity to OOP ‘methods’.)
  • That is, same most general type.
  • If two different developers by accident produce same-named functions, they’re presumably from two different applications (or separate functional areas within a large application). Then they’re not “similar” in any semantic sense/they shouldn’t be combined into one class/that’s exactly what the module system is for to manage name-clashes.
  • (One of the proposals has the example of Succ appearing in separate topic areas. Yes, there’s reasonable grounds for each separate developer to want to call their constructor Succ. No Haskell doesn’t have to bend over backwards because the developers can’t/won’t talk to each other.)

… or types. If they’re “similar” named but not same named, no problem for the compiler. (Human readers might confuse them.) If they have different type/arity or arguments, they’re not candidates for using the typeclass mechanism. I have no idea why your Example 1 wants deliberately to give two different functions the same name fromV2. It seems wilfully perverse (in Haskell – is this some trick adapted from OOP?). Just don’t do that.

And similarly with #283, it’s up to Library maintainers what qualified name they choose to export. They can choose names that “apply well to each” topic area [quoting #283]. And knowing the nature of Haskell libraries, those’ll be high-level/abstract/generic abbreviations from Algebra/Category Theory or some such – like Set, Coll, Fin, Zero, Succ.

On the face if it, this eases the burden on importers: they don’t have to allocate qualifiers/namespaces to each import; they’ll get consistent naming across their project. But …

Importing two libraries addressing the same topic area, they’re likely to use the same abstract/generic names of the module/namespace. So there’s still a clash. And still the importer has to allocate overriding [**] module/namespace names. Now that’s unexpected for others reading/using that code.

[**] #283 is unclear how you override a qualified export. Is that the existing import qualified ... as ...? without drawing attention that it’s overriding?

And how does this namespace proposal handle clashes of namespace names in the importing module?

Great question. I’ll add an example.

  1. The proposal allows for importing modules into namespaces directly:
    import namespace Data.Category as Category
    import namespace Essential.CategoryTheory as Essential
    
    convert :: Category:Cat:TheCat -> Essential:Cat:TheCat
    convert = ..
    
  2. You can rename namespaces, It’s a little akward because I don’t want the proposal to add too much syntatic sugar, which might conflict with other parts of GHC:
    -- continuing
    namespace RealCat = Category:Cat
      where open Category:Cat
    namespace OtherCat = Essential:Cat 
      where open Essential:Cat   
    
    (edit) I have changed the proposal to accept:
    namespace RealCat = Category:Cat
    namespace OtherCat = Essential:Cat 
    
    which is alot cleaner and consistent with the type X = Y syntax.
  3. The proposal suggest that another langauge extension (say NamedExportImports) should enable renaming of all entities and functions on import or export
    import Data.Category (Cat as RealCat)
    import Essential.CategoryTheory (Cat as OtherCat)
    

By the way, the proposal is now live at ghc-proposals if any any of you want to follow along.