2026/01/18

2026-01-18 00:00:53 +0100 <haskellbridge> <Man of Letters (Mikolaj)> the idea is (and it works and speeds up runtime considerably) that I add lots of INLINEs and INLINEABLE and then in each line starting with "Is @<some type>" a function is either inlined or called (but with unfolding available), which GHC now sees operating at concrete type, so it specializes the function (or its inlining) to that specific type
2026-01-18 00:01:30 +0100 <haskellbridge> <Man of Letters (Mikolaj)> this is a code base with a huge amount of existential types, which totally block any sane ways of specializing code
2026-01-18 00:01:44 +0100 <haskellbridge> <Man of Letters (Mikolaj)> * specializing/monomorphizing
2026-01-18 00:02:06 +0100humasect(~humasect@dyn-192-249-132-90.nexicom.net) (Quit: Leaving...)
2026-01-18 00:02:38 +0100merijn(~merijn@host-cl.cgnat-g.v4.dfn.nl) (Ping timeout: 260 seconds)
2026-01-18 00:02:52 +0100 <haskellbridge> <Man of Letters (Mikolaj)> but if anybody has any other ideas how to avoid passing around explicit dictionaries but maintain the very abstract API, I'm all ears
2026-01-18 00:05:18 +0100 <haskellbridge> <Man of Letters (Mikolaj)> because what I'm currently doing is seriously embarrassing
2026-01-18 00:05:34 +0100 <EvanR> any time you pass around explicit dictionaries, can't it also be passing (perhaps hidden inside a data type) a record of things doing the implementation
2026-01-18 00:06:38 +0100 <haskellbridge> <Man of Letters (Mikolaj)> that makes sense; let me think why not
2026-01-18 00:07:35 +0100ethantwardy(~user@user/ethantwardy) ethantwardy
2026-01-18 00:09:58 +0100ethantwardy(~user@user/ethantwardy) (Client Quit)
2026-01-18 00:11:40 +0100target_i(~target_i@user/target-i/x-6023099) (Quit: leaving)
2026-01-18 00:13:13 +0100merijn(~merijn@host-cl.cgnat-g.v4.dfn.nl) merijn
2026-01-18 00:14:14 +0100ethantwardy(~user@user/ethantwardy) ethantwardy
2026-01-18 00:15:01 +0100ethantwardy(~user@user/ethantwardy) (Client Quit)
2026-01-18 00:16:51 +0100 <haskellbridge> <Man of Letters (Mikolaj)> so, the only argument against I can find is circularity: say, I'm implementing indexing in fancy nested arrays (which is called 12! times in the benchmark I'm working on), the code for the indexing takes a dictionary for storable vectors (the arrays are represented as tuples of such vectors) and it's terribly slow when it has to access the dictionary each time it's called
2026-01-18 00:17:26 +0100 <haskellbridge> <Man of Letters (Mikolaj)> if I understand your idea correctly, the nested array indexing code should instead take a dictionary that directly works on nested arrays, not vectors
2026-01-18 00:18:13 +0100 <haskellbridge> <Man of Letters (Mikolaj)> but the nested array indexing code implements what should be in that dictionary --- I'd need a way to somehow boostrap this, recurse across a dictionary
2026-01-18 00:18:24 +0100merijn(~merijn@host-cl.cgnat-g.v4.dfn.nl) (Ping timeout: 260 seconds)
2026-01-18 00:18:34 +0100 <EvanR> holy moly
2026-01-18 00:21:01 +0100 <EvanR> if you're combining implementations in a way to implement another implementation, you do want to use prudent "let" bindings to make sure you only access the dependent implementation once
2026-01-18 00:21:14 +0100 <EvanR> instead of every time it's accessed
2026-01-18 00:21:38 +0100 <EvanR> this assumes the dependent implementation isn't dynamically changing for the same data structure
2026-01-18 00:22:27 +0100 <EvanR> dependency isn't dynamically changing*
2026-01-18 00:22:50 +0100 <monochrom> Oh, like "f x = f (x - 1) + f (x - 1)" vs "f x = let y = f (x - 1) in y + y"?
2026-01-18 00:22:54 +0100 <EvanR> as far as knot typing I don't know about this
2026-01-18 00:22:58 +0100 <EvanR> tying
2026-01-18 00:24:10 +0100 <EvanR> makeF add x = add (x - 1) (x + 1)
2026-01-18 00:24:20 +0100 <EvanR> let f = makeF (+)
2026-01-18 00:24:38 +0100 <EvanR> this might not apply exactly, I'm not sure I totally understand the situation
2026-01-18 00:24:38 +0100 <Leary> Man of Letters (Mikolaj): Re monomorphising, I see, that's a bit unfortunate. You can perhaps get rid of those existentials by using suitable heterogeneous types instead.
2026-01-18 00:24:53 +0100ethantwardy(~user@user/ethantwardy) ethantwardy
2026-01-18 00:25:34 +0100 <haskellbridge> <Man of Letters (Mikolaj)> Leary: where can I see an example of something like that?
2026-01-18 00:27:55 +0100troydm(~troydm@user/troydm) troydm
2026-01-18 00:28:07 +0100 <haskellbridge> <Man of Letters (Mikolaj)> some of the existential types arise naturally from AST GADTs (I have multiple interpreters of deeply embedded DLS in the implementation)
2026-01-18 00:29:01 +0100merijn(~merijn@host-cl.cgnat-g.v4.dfn.nl) merijn
2026-01-18 00:30:01 +0100 <Leary> The classic example is `data HList f xs where { Nil :: HList f '[]; Cons :: f x -> HList f xs -> HList f (x:xs) }`, but you can do similarly for arbitrary ASTs by duplicating some of their structure on the type level in the same way.
2026-01-18 00:30:03 +0100 <haskellbridge> <Man of Letters (Mikolaj)> e.g., I can't think how to get rid of the existentially bound types in the node
2026-01-18 00:30:03 +0100 <haskellbridge> ... long message truncated: https://kf8nh.com/_heisenbridge/media/kf8nh.com/QetWxmpnbhshlldjxsPntKNj/OFJ7l1DSftE (5 lines)
2026-01-18 00:30:32 +0100 <haskellbridge> <Man of Letters (Mikolaj)> ok, thank you, I will think about this
2026-01-18 00:31:55 +0100ethantwardy(~user@user/ethantwardy) (Quit: WeeChat 4.4.2)
2026-01-18 00:32:18 +0100 <haskellbridge> <Man of Letters (Mikolaj)> EvanR: right, so that's precisely the purpose of my crude hack with duplicating functions (or function calls) and so helping GHC specialize each call at a concrete known type; let me explain:
2026-01-18 00:33:13 +0100 <haskellbridge> <Man of Letters (Mikolaj)> in your example, by the well placed "let", you avoid one of the two applications of subtraction
2026-01-18 00:33:51 +0100 <haskellbridge> <Man of Letters (Mikolaj)> in my case, by specializing/monomorphising, I get rid of more than one dictionary lookup/passing
2026-01-18 00:34:08 +0100 <EvanR> that was monochroms version
2026-01-18 00:34:21 +0100 <haskellbridge> <Man of Letters (Mikolaj)> oh, yes, right
2026-01-18 00:34:33 +0100Inline(~User@2001-4dd6-dd24-0-d5a6-802e-e6e6-ce59.ipv6dyn.netcologne.de) (Ping timeout: 256 seconds)
2026-01-18 00:34:55 +0100ethantwardy(~user@user/ethantwardy) ethantwardy
2026-01-18 00:35:20 +0100 <EvanR> I was thinking that, for a given properly constructed implementation, you only do look ups into any implementation once per call to the API
2026-01-18 00:35:43 +0100merijn(~merijn@host-cl.cgnat-g.v4.dfn.nl) (Ping timeout: 240 seconds)
2026-01-18 00:35:44 +0100 <EvanR> for any of the types that have this api
2026-01-18 00:35:47 +0100 <haskellbridge> <Man of Letters (Mikolaj)> anyway, sadly, while "let" can cache values, it can't cache the determination which dictionary to look up into and how to combine whatever has been obtained from the dictionary --- only the GHC magic can do that
2026-01-18 00:36:01 +0100 <EvanR> :thonk:
2026-01-18 00:36:28 +0100Zemy_(~Zemy@2600:100c:b04a:cc3c:2826:1bff:fef2:30a6)
2026-01-18 00:37:22 +0100 <EvanR> I would guess whatever ghc magic that is can be "manually" coded into the implementations, but no bearing on how the effort compares
2026-01-18 00:37:38 +0100Core3498(~Zemy@72.178.108.235)
2026-01-18 00:37:40 +0100 <haskellbridge> <Man of Letters (Mikolaj)> and the same regarding knots --- Haskell recursion doesn't cut it, something involving GHC internals would be needed (or something totally out of the box like maybe the heterogeneous types idea once I understand it)
2026-01-18 00:38:03 +0100 <haskellbridge> <Man of Letters (Mikolaj)> oh, yes, sure, e.g., you can manually write monomorphic code from the start
2026-01-18 00:38:17 +0100 <haskellbridge> <Man of Letters (Mikolaj)> and I actually like that a lot
2026-01-18 00:38:30 +0100 <EvanR> not monomorphic
2026-01-18 00:39:01 +0100 <EvanR> polymorphic types can satisfy the same API as monomorphic types
2026-01-18 00:39:02 +0100trickard(~trickard@cpe-82-98-47-163.wireline.com.au) (Read error: Connection reset by peer)
2026-01-18 00:39:14 +0100Core4452(~Zemy@2600:100c:b04a:cc3c:ac56:f4ff:fe3c:1c26)
2026-01-18 00:39:15 +0100trickard_(~trickard@cpe-82-98-47-163.wireline.com.au)
2026-01-18 00:39:42 +0100Zemy_(~Zemy@2600:100c:b04a:cc3c:2826:1bff:fef2:30a6) (Read error: Connection reset by peer)
2026-01-18 00:39:44 +0100 <EvanR> e.g. addition interface could be implemented by Word8 or Complex a where a implements addition interface
2026-01-18 00:40:04 +0100 <haskellbridge> <Man of Letters (Mikolaj)> yes, you are right, code without existentials, plus "-fexpose-overloaded-unfoldings" and "-fspecialise-aggressively" should in theory be just as good at avoiding runtime lookups
2026-01-18 00:40:12 +0100Zemy(~Zemy@72.178.108.235) (Ping timeout: 256 seconds)
2026-01-18 00:41:10 +0100 <EvanR> aiui typeclasses shine in cases where you can get away without talking about explicit dictionaries, since it will be passed automagically around
2026-01-18 00:41:26 +0100 <haskellbridge> <Man of Letters (Mikolaj)> yes, I fully agree
2026-01-18 00:41:45 +0100bgamari(~bgamari@64.223.170.198) (Quit: ZNC 1.8.2 - https://znc.in)
2026-01-18 00:41:49 +0100Core3498(~Zemy@72.178.108.235) (Ping timeout: 246 seconds)
2026-01-18 00:43:32 +0100 <haskellbridge> <Man of Letters (Mikolaj)> but once you start writing GADTs it's so hard to avoid existentials; I tried for a while, marked each one in the source code, but quickly gave up --- too many :)
2026-01-18 00:44:07 +0100ethantwardy(~user@user/ethantwardy) (Quit: WeeChat 4.4.2)
2026-01-18 00:44:15 +0100vanishingideal(~vanishing@user/vanishingideal) vanishingideal
2026-01-18 00:44:27 +0100bgamari(~bgamari@64.223.173.201)
2026-01-18 00:44:29 +0100 <geekosaur> enh, you can enable GADTSyntax without enabling GADTs
2026-01-18 00:45:40 +0100 <haskellbridge> <Man of Letters (Mikolaj)> oh no, but the sweet reward is in the extra type correctness real GADTs ensure, unlike GADTSyntax, especially for syntax-like things
2026-01-18 00:46:18 +0100 <haskellbridge> <Man of Letters (Mikolaj)> I'd love to read some functional pearl that rewrites the classic GADT AST examples without existentials in some fancy way
2026-01-18 00:46:36 +0100 <haskellbridge> <Man of Letters (Mikolaj)> in many plausible ways, preferably
2026-01-18 00:46:52 +0100 <haskellbridge> <Man of Letters (Mikolaj)> with similar correctness guarantees
2026-01-18 00:47:03 +0100merijn(~merijn@host-cl.cgnat-g.v4.dfn.nl) merijn
2026-01-18 00:47:58 +0100 <EvanR> the GADTsyntax is besides the point, since the key thing is case analyzing to introduce the forgotten now unknown type... however it was defined and constructed
2026-01-18 00:48:07 +0100 <haskellbridge> <Man of Letters (Mikolaj)> for a non-trivial AST (I don't remember Peano arithmetic or lambda calculus already have existential types in the classic encoding)
2026-01-18 00:48:08 +0100 <geekosaur> I'm confused. Doesn't that extra correctness come specifically from the embedded existentials, which are exposed by scrutinizing constructors?
2026-01-18 00:48:10 +0100 <EvanR> in cases where you use existentials
2026-01-18 00:49:00 +0100 <haskellbridge> <Man of Letters (Mikolaj)> geekosaur: I'm afraid so, but maybe there is another way?
2026-01-18 00:49:15 +0100 <EvanR> doing existentials without existentials sounds tricky
2026-01-18 00:49:25 +0100 <EvanR> (and what's the point)
2026-01-18 00:49:30 +0100 <haskellbridge> <Man of Letters (Mikolaj)> :D
2026-01-18 00:50:23 +0100humasect(~humasect@dyn-192-249-132-90.nexicom.net) humasect
2026-01-18 00:50:59 +0100ethantwardy(~user@user/ethantwardy) ethantwardy
2026-01-18 00:51:37 +0100merijn(~merijn@host-cl.cgnat-g.v4.dfn.nl) (Ping timeout: 246 seconds)
2026-01-18 00:52:51 +0100 <haskellbridge> <Man of Letters (Mikolaj)> well, the point is, they are not a zero-cost abstraction unlike, in principle, most of other abstractions Haskell provides
2026-01-18 00:53:47 +0100 <haskellbridge> <Man of Letters (Mikolaj)> and being so handy, they easily pollute the performance-sensitive parts of the application
2026-01-18 00:55:03 +0100cattieskitties
2026-01-18 00:55:27 +0100 <geekosaur> I think the only other alternative isn't here yet: dependent type witnesses of some kind. Which are also not zero cost, and I suspect end up being just a different way to encode existentials
2026-01-18 00:56:30 +0100 <haskellbridge> <Man of Letters (Mikolaj)> ;<
2026-01-18 00:57:23 +0100trickard_(~trickard@cpe-82-98-47-163.wireline.com.au) (Read error: Connection reset by peer)
2026-01-18 00:57:37 +0100trickard_(~trickard@cpe-82-98-47-163.wireline.com.au)
2026-01-18 00:58:10 +0100 <geekosaur> (GADT-style, at least)
2026-01-18 00:58:18 +0100__monty__(~toonn@user/toonn) (Quit: leaving)
2026-01-18 01:02:47 +0100merijn(~merijn@host-cl.cgnat-g.v4.dfn.nl) merijn
2026-01-18 01:07:19 +0100merijn(~merijn@host-cl.cgnat-g.v4.dfn.nl) (Ping timeout: 240 seconds)
2026-01-18 01:08:00 +0100 <Leary> Man of Letters (Mikolaj): Re the heterogeneous types, it's basically just a matter of building a home for your discarded types to live in. In the simplest case where all those types have the same kind, you could literally just add a type level list to your AST: `AstCastS :: (NumScalar r1, RealFrac r1, NumScalar r2, RealFrac r2) => AstTensor discarded ms s (TKS sh r1) -> AstTensor (r1:discarded) ms s (TKS sh r2)`
2026-01-18 01:10:55 +0100ljdarj(~Thunderbi@user/ljdarj) (Ping timeout: 244 seconds)
2026-01-18 01:14:31 +0100qqq(~qqq@185.54.21.105) (Quit: Lost terminal)
2026-01-18 01:17:09 +0100merijn(~merijn@host-cl.cgnat-g.v4.dfn.nl) merijn
2026-01-18 01:18:32 +0100 <haskellbridge> <Man of Letters (Mikolaj)> oh wow, thank you, that's interesting
2026-01-18 01:20:00 +0100mhatta(~mhatta@www21123ui.sakura.ne.jp) (Quit: ZNC 1.10.1+deb1 - https://znc.in)
2026-01-18 01:20:19 +0100 <haskellbridge> <Man of Letters (Mikolaj)> falls more into the category of "ways to make existential-like things zero-cost" than the category "an alternative but similarly handy abstraction mechanism"
2026-01-18 01:21:35 +0100takuan(~takuan@d8D86B9E9.access.telenet.be) (Ping timeout: 245 seconds)
2026-01-18 01:22:14 +0100merijn(~merijn@host-cl.cgnat-g.v4.dfn.nl) (Ping timeout: 265 seconds)
2026-01-18 01:24:23 +0100poscat(~poscat@user/poscat) (Remote host closed the connection)
2026-01-18 01:25:43 +0100mhatta(~mhatta@www21123ui.sakura.ne.jp)
2026-01-18 01:27:33 +0100poscat(~poscat@user/poscat) poscat
2026-01-18 01:31:08 +0100trickard_trickard
2026-01-18 01:32:56 +0100merijn(~merijn@host-cl.cgnat-g.v4.dfn.nl) merijn
2026-01-18 01:38:19 +0100merijn(~merijn@host-cl.cgnat-g.v4.dfn.nl) (Ping timeout: 260 seconds)
2026-01-18 01:38:48 +0100machinedgod(~machinedg@d75-159-126-101.abhsia.telus.net) machinedgod
2026-01-18 01:43:04 +0100vanishingideal(~vanishing@user/vanishingideal) (Ping timeout: 246 seconds)
2026-01-18 01:43:05 +0100droideqa(uid499291@user/droideqa) (Quit: Connection closed for inactivity)
2026-01-18 01:43:25 +0100Zemy(~Zemy@72.178.108.235)
2026-01-18 01:44:59 +0100vanishingideal(~vanishing@user/vanishingideal) vanishingideal
2026-01-18 01:47:16 +0100Core4452(~Zemy@2600:100c:b04a:cc3c:ac56:f4ff:fe3c:1c26) (Ping timeout: 246 seconds)
2026-01-18 01:48:43 +0100merijn(~merijn@host-cl.cgnat-g.v4.dfn.nl) merijn
2026-01-18 01:50:11 +0100 <monochrom> Existentials are zero-cost iff unusable.
2026-01-18 01:51:58 +0100 <monochrom> an easy corollary of information theory. For a value to be usable, you must pass around sufficient information, thus the non-zero cost.
2026-01-18 01:53:15 +0100merijn(~merijn@host-cl.cgnat-g.v4.dfn.nl) (Ping timeout: 240 seconds)
2026-01-18 01:53:34 +0100 <EvanR> a value can have zero cost if the only possibility is that it exists i.e. ()
2026-01-18 01:53:52 +0100 <EvanR> sounds useful
2026-01-18 01:54:07 +0100 <monochrom> A concrete example being: If you give me an existential value and want me to be able to use Show methods on it, then you must also give me the Show methods, lest how do I even know that I'm allowed to.
2026-01-18 01:55:30 +0100 <monochrom> or at least a pointer to the struct of Show methods.
2026-01-18 01:57:12 +0100 <monochrom> OK OK s/unusable/indistinguishable from ()/ :)
2026-01-18 02:00:09 +0100Zemy_(~Zemy@2600:100c:b04a:cc3c:1015:34ff:fe46:86ce)
2026-01-18 02:03:18 +0100vanishingideal(~vanishing@user/vanishingideal) (Ping timeout: 252 seconds)
2026-01-18 02:03:18 +0100Zemy(~Zemy@72.178.108.235) (Ping timeout: 252 seconds)
2026-01-18 02:03:30 +0100Zemy(~Zemy@72.178.108.235)