Pre-GHC-Proposal: Instantiate backpack signatures when importing

5 Likes

Usually the main reason that something is not used is that something is not useful. What’s the purpose of Backpack? Clearly its original destiny to solve the String problem flopped.

1 Like

My reason for thinking about it again is this issue:

If Backpack was more mature, then the duplication between IntMap and Word64Map would not really be an issue at all.

1 Like

CPP is good enough for such things, look how filepath (or tests of bytestring) avoids duplication.

3 Likes

Exactly! The UX around Backpack isn’t great and that’s why it has remained niche & unused. I think to say anything more (like that Backpack’s fundamental goals and capabilities are now proven to not be useful) is a bit much given the UX situation.

One usage of backpack that we could really use in the ecosystem is unboxed containers. They exist with Backpack but the way you use them is ugly and a pain.

Basically, I like this proposal in theory because it makes Backpack more of a first-class citizen.

1 Like

It’s possible to make it work with CPP, but do you really think CPP is a better way to solve these kinds of issues than to use Backpack (if Backpack were more mature)?

Yes, I do. This kind of issues is precisely what macros are for. No need to invent an arcane technology for something that works out of the box for half a century.

2 Likes

Aren’t the flaws of CPP obvious, e.g. these from http://blog.haskell-exists.com/yuras/posts/stop-abusing-cpp-in-haskell.html:

CPP is a C preprocessor, but it is common to use it in Haskell. That leads to a number of issues.

  • It can mess with haskell code.

CPP doesn’t understand Haskell code, instead it assumes C code. It is free to remove insignificant (for C, not for Haskell) whitespace, expand macros in Haskell comments and strings or mess with identifiers that contain ' or #.

Every time you change your .cabal file, e.g. add new module, or update dependencies, cabal regenerates cabal-macros.h file. Then the recompilation checker pessimistically decides to recompile all modules with CPP enabled.

  • It makes automatic code analyzing and transforming harder.

If you use hlint or HaRe, then you probably know what I mean.

And I’d like to add that CPP is not typed (not even always syntactically sound) and the error messages will often become hard to understand because they are about the resulting file after macro expansion, not about the source file that you write (at best the source locations might be shifted to their proper place). Edit: add broken Haddock source links to the list.

I understand wanting to avoid complexity, but the problems do pile up after half a century.

Also, ML-style modules are also around 40 years old by now, so the ideas behind Backpack are not particularly new.

2 Likes

shrug There is a simple working solution for your use case in containers, attested in other boot libraries. But if you prefer to wait 5+ years until containers drop support of all existing versions of GHC to be able to rely on improved Backpack, you are free to do so indeed.

I bet that Backpack error messages are way harder to understand :wink:

I use backpack extensively in my game engine.
I wanted to implement a “small” core of the engine (in the style of GHC.Core) that would be abstract in the renderer implementation.

I thought a long while about it and tried going multiple different paths before I realized what I really needed was backpack.

  • The “core engine” is then defined in terms of abstract renderer interfaces which define loads of rendering-specific abstract datatypes (say DescriptorSet, GraphicsPipeline) and many more functions on them.
  • Then, I have a vulkan implementation of these abstract interfaces.
  • Finally, the “big, batteries-included, engine” instances the Core signatures with vulkan implementation and it all works great (except for up until it actually does: HLS, cabal, ghc bugs…) — but when it’s finally working it’s going really great.

Despite trying, it was completely impractical, not to mention less performant (compile and likely runtime wise), to have a MonadRenderer. It would have to define too many type families, way too many functions, and then all calls to these functions on “Core” would have to wrap the monad type parameter (DescriptorSet m, GraphicsPipeline m, etc etc)

Etc etc etc, I have a lot to say about what it bought me in terms of abstraction and extensibility (and potential for “””easily”””swapping the renderer implementation), but I’ll wait until I have a good enough version 0.1

Anyway!

8 Likes

The unique selling point of Backpack is that potential instantiations are unlimited, “open world”. Practically however one is more likely to deal with a finite set of possibilities, such as String, PosixString and WindowsString in filepath; IntSet, Int64Set, WordSet and Word64Set in containers; or a fairly limited list of rendering engines. If CPP feels too low level, Cabal flags switching between packages and mixins can work better on large scale.

2 Likes

unboxed containers are a perfect “open world” example actually - arbitrary users want their arbitrary record UNPACKed in a container type instead of being boxed

4 Likes

I am convinced that Backpack addresses an important problem. I think the primary reason it has remained “niche and unused” is that Backpack lacks a champion: a person (or preferably several) who is excited to get into the details, fix the UX, write blogs and documentation, and generally knock down the barriers to entry.

Maybe someone reading this thread feels motivated to step up to that challenge?

Simon

11 Likes

I bet that Backpack error messages are way harder to understand :wink:

I was curious, so I took this example and introduced some errors to see what they looked like.

Leaving the Str module signature unfilled here:

mixins:
...
lesson2-signatures (Lesson2 as Lesson2.Text) 

The error:

    Non-library component has unfilled requirements: Str
    In the stanza 'executable lesson2'
    In the inplace package 'lesson2-signatures-1.0.0.0'

Giving an implementation type here a kind different to what the module signature specifies:

type Str = Maybe

splitOn :: Char -> Maybe () -> Maybe ()
splitOn c = undefined

The error:

    • Type constructor ‘Str’ has conflicting definitions in the module
      and its hsig file
      Main module: type Str :: * -> *
                   type Str = GHC.Maybe.Maybe :: * -> *
      Hsig file:  type Str :: *
                  data Str
      The types have different kinds
    • while checking that lesson2-signatures-1.0.0.0:impl-text:Str.Text implements signature Str in lesson2-signatures-1.0.0.0[Str=lesson2-signatures-1.0.0.0:impl-text:Str.Text]

Removing the function splitOn from the implementation module here so that it can’t correctly instantiate the module signature.

The error:

lib/Str.hsig:1:1: error:
    • ‘splitOn’ is exported by the hsig file, but not exported by the implementing module ‘lesson2-signatures-1.0.0.0:impl-text:Str.Text’
    • while checking that lesson2-signatures-1.0.0.0:impl-text:Str.Text implements signature Str in lesson2-signatures-1.0.0.0[Str=lesson2-signatures-1.0.0.0:impl-text:Str.Text]
  |
1 | signature Str where
1 Like

Those first two are pretty good error messages. The last one could use some improvement. Edit: I didn’t read correctly that you actually deleted the whole function. In that case the last message also seems to be accurate.

I think it would be interesting if someone actually were to rewrite that CPP code of filepath/bytestring etc. with backpack and demonstrate that it’s nicer and easier to maintain.

The other argument that comes up frequently is a common API for containers without the overhead of typeclasses.

I think @ChShersh has blogged about it here: Picnic: put containers into a backpack :: Kowainik

I still find most of the examples not too convincing. Maybe I Iack imagination?

1 Like

If we’re spitballing alternative methods of achieving backpack…

You could possibly also use convenience internal libraries, make them share the same source module that you want to have multiple implementations, but each import their parameter (types, implementations) from a module in isolated source directories. To instantiate them you’d use PackageImports. No CPP required (although could be used).

That would serve the local project case, but wouldn’t serve the open world case that backpack does.

1 Like

At my previous job on a smaller codebase with a performance-constrained simulator, we wanted to use backpack.

The reason we didn’t is because HLS wouldn’t work well with backpack, and we relied on HLS to be productive.

Beyond that we were also concerned about sharp edges that might bite us. In the end we chose dependency injection via passing records (which has a runtime overhead ofc).

3 Likes

Arguably GHC could have specialised polymorphic types to reap benefits of unpacked strict fields automatically.

Have you seen any comprehensive benchmarks for Backpack-ed containers? Tree-like structures eat a ton of extra memory anyways, so memory savings from unboxing keys are unlikely to be significant. The real benefit could come from instance Ord key inlined and operating on unboxed Int#, but you get the same with IntMap (if you keys are small enough) or HashMap (if your keys are larger).

What about GC pause gains? It seems to me it would cut the amount of scanning the GC does for live containers. For most workloads and use-cases, it’s probably not interesting. But I suspect it can make a difference for large containers that are dynamic (cannot use compact) + have actual tight latency budgets (e.g. not your average Haskell production web app).