haskell / lsp

Haskell library for the Microsoft Language Server Protocol
360 stars 89 forks source link

Consider switching to microlens or overloaded dot syntax? #465

Open luc-tielen opened 1 year ago

luc-tielen commented 1 year ago

Hi,

I saw that lsp has a dependency on lens, which is a fairly large dependency. Would it be possible to swap it out for an alternative such as overloaded dot syntax or microlens? I'm asking this because my compiler's build time almost doubled just including this library.

Are there any things blocking this? Or is there another reason behind it?

michaelpj commented 1 year ago

I think the main issue is that we're very reliant on two things: a) the lens classy lenses and b) the lens auto-generation machinery.

a) is important because there is a lot of name reuse in the LSP spec. We really need overloaded field accessors, and with lens that means using the classy lenses AFAIK. b) is important because there are so many types, there is no way we could write them by hand.

Maybe once I finally get https://github.com/haskell/lsp/pull/458 over the line we could consider writing our own lens generation machinery... but that seems like a lot of work :/ Alternatively I guess something like generic-lens might work. Any other ideas?

luc-tielen commented 1 year ago

I have to admit I'm not that experienced with lenses and such. :sweat_smile: I found this recent thread on reddit: https://www.reddit.com/r/haskell/comments/z0czfc/the_modern_lens_setup_genericlens/ Maybe optics-core provides enough functionality for what you need? (See one of the replies.)

phadej commented 1 year ago

In any particularly big development you'll have lens. Having microlens will only add microlens.

Consider the install plan for haskell-language-server-1.7.0.0 with GHC-9.0.2.

I'm actually somewhat surprised that there aren't more dependencies on lens.

In the big picture lens is a small library. E.g. profunctors and semigroupoids are needed by stm-containers. free (which also needs profunctors and semigroupoids) is used by ghc-exactprint. So "big" kmett's lens dependencies are already there. And then vector, unordered-containers, ... are depended upon other stuff, e.g. aeson.

TL;DR if you change lens to microlens in one place, you just add microlens to the mix.


EDIT: generic-lens is slow. Compile-time is slow at use sites, as well as there is no performance guarantees. TH generated lenses are more predicatable in both regards.

michaelpj commented 1 year ago

I'm considering switching to generic-lens. Reasons:

I don't know how bad this would be downtream, though.

michaelpj commented 1 year ago

Trying it out it seems that https://github.com/kcsongor/generic-lens/issues/96 would be annoying for us.

phadej commented 1 year ago

Lets us drop the creation and compilation of hundreds of lens declarations from lsp-types

But then you'll do a lot of generics evaluation at compile time at each use site (and GHC is slow to do that). It's far from clear it is a net win (especially if lsp-types is relatively more stable part of codebase).

michaelpj commented 1 year ago

Yes, I agree the compilation time is maybe a wash. Honestly the thing I find most pleasing is being able to get away from prefixing all the fields with _...

phadej commented 1 year ago

With NoFieldSelectors you can have lens and no _ prefix (using custom LensRules and makeLensesWith):

{-# LANGUAGE NoFieldSelectors, TemplateHaskell #-}

import Control.Lens
import Language.Haskell.TH (mkName, nameBase)

data Foo = MkFoo { int :: Int, char :: Char} deriving Show

$(let namer _ty _ns n = [TopName $ mkName $ nameBase n] -- identity namer
      rules =  lensRules & lensField .~ namer
  in makeLensesWith rules ''Foo)

main :: IO ()
main = do
    print $ (MkFoo 21 'x')
      & char .~ 'y'
      & int  %~ (* 2)

Tested with GHC-9.2.7.

arybczak commented 1 year ago

FYI, generic lenses and prisms from optics-core are well-optimized for compile time evaluation (much better than generic-lens, some measurements are available here) and have better ergonomics.

As for the runtime performance, generic lenses at least for pure products always (well, up to 100 fields at minimum) optimize well (this is ensured by tests from https://gitlab.haskell.org/ghc/ghc/-/merge_requests/2965), for sums it's more restrictive (more details are in the MR if you're interested).

A long time ago I've switched a large codebase with about 3k use sites of lenses from TH generated optics to generic optics and compile times with optimizations were pretty much the same (about 15% slower without optimizations).