- Notifications
You must be signed in to change notification settings - Fork572
Implement Typeable#4097
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
base:master
Are you sure you want to change the base?
Implement Typeable#4097
Uh oh!
There was an error while loading.Please reload this page.
Conversation
rhendric commentedMay 29, 2021 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
I'm reading the conversation in#2243 and, like several of the participants there, I've never had the need for Is that basically right? If so, my current weakly-held opinion is that I'd rather spend compiler code on making working with rows and |
The I think the big difference though is that variants are tagged with their constructor in the encoding, but |
The |
TheMatten commentedMay 29, 2021 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
@garyb yeah, problems with subcomponents are precisely what I'm working with myself - for context, I'm currently porting Shpadoinkle (https://shpadoinkle.org) into purescript (https://gitlab.com/thematten/purescript-fluorine). Now, thing is, premise of it's approach is to make composition simple, and while that's easily doable with simple trees of elements, problems arise once you try to add memoization, because while patching, you have no way to know whether new memoized node isn't actually a completely different one, with different type of argument. Halogen solves this with typed approach as mentioned, but such solution degrades user experience a lot, introducing need for additional boilerplate, possibility of new, unproductive type errors and leaks internal details about components being memoized. All of that only to tell compiler which memoized types may appear in some position. And with runtime representation being basically the same, as@rhendric mentioned. This may bite programmers in pretty arbitrary situations, e.g. during refactoring of components. In Haskell, |
timjs commentedMay 30, 2021
I'd love to have parse::Text->EitherTextDynamicparse val|Just v<- scan val::MaybeUnit= okay<|Dynamic v|Just v<- scan val::MaybeBool= okay<|Dynamic v|...|otherwise=error<|unwords ["!! Error parsing value", display val|> quote]insert::Componenta->Dynamic->Componentainsert (Field b) (Dynamic b')=|JustRefl<- b~= b'-> done<|Field b'|otherwise-> throw<|CouldNotChangeVal (someTypeOf b) (someTypeOf b')... I'm not aware of a trick using open variants to achieve the same thing... |
TheMatten commentedMay 30, 2021 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
@timjs Yeah, once again you could probably create some top level synonym used in place of Really, |
timjs commentedJun 1, 2021 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
@TheMatten, that is indeed a possibility, but it assumes aclosed world doesn't it? In my example above, the library in which the The application, containing the As a library author, I don't want to make that choice upfront. So closed variants are not an option. Or is there a way an open variant could model that? |
@timjs I imagine you should be able to parameterize context you work in with some variant supplied by the user, similarly to how slots in What I'm trying to say is that while it's hard to come up with usecase which wouldn't be possible to encode with "big enough" |
It does not follow that the maintenance cost is low just because this PR has only 60 lines. The potential maintenance cost of supporting an extra feature which ends up being depended on by users is very difficult to predict, but could be significantly more than you might expect. If this implementation turns out to be incorrect, we'll need to diagnose and fix the bug - and sometimes this significantly complicates the implementation. The initial Coercible PR seemed fine but it needed something like 10 followup PRs to get the feature to a stage where it worked correctly, and by the end I think we had quite a bit more code than we did originally. The feature needs documentation, it generates more questions along the lines of "when should I use feature X rather than feature Y", it could interact poorly with other features, etc. |
TheMatten commentedJun 1, 2021 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
@hdgarrood Yeah, I should probably be more concrete about maintenance costs:
|
I am familiar with Variant, but less familiar with Typeable - since here we're talking about implementing a compiler extension, I'm wondering:
|
TheMatten commentedJun 1, 2021 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
@f-f To answer your points:
|
I'm 👎 because I don't think this provides enough utility/is in enough demand to justify adding a feature to the compiler, sorry. |
@hdgarrood no problem 🙂 Alternatively, would it be possible to expose needed information e.g. in#4092? |
So adding type information to CoreFn? That is something that comes up from time to time, but I think it's a bigger project than it seems. I don't really know enough about it to be able to comment unfortunately. |
timjs commentedJun 1, 2021
Sorry, I've lost the discussion on To make it concrete. Say I've some container type, which is empty or filled with a value of type {-#LANGUAGE GADTs #-}{-#LANGUAGE ScopedTypeVariables #-}{-#LANGUAGE TypeOperators #-}moduleTypeablewhereimportData.Type.EqualityimportType.Reflection(?=)::TypeRepa->TypeRepb->Maybe (a:~:b)(?=)= testEquality--| A container of type `a`dataContainera=Empty |Filleda--| Essentialy `Dynamic`dataInputwhereInput::Typeablea=>a->Inputupdate::foralla.Typeablea=>Containera->Input->Containeraupdate (Empty) (Input y)|JustRefl<- alpha?= typeOf y=Filled y|otherwise=Emptywhere alpha= typeRep::TypeRepaupdate (Filled x) (Input y)|JustRefl<- typeOf x?= typeOf y=Filled y|otherwise=Filled x Above is working Haskell code. I'd do the same thing in Idris by using a sigma type saving a type witness. I can't see how I would use a |
rhendric commentedJun 1, 2021 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
importPreludeimportData.Maybe (Maybe(..))importData.Symbol (classIsSymbol)importData.Variant (Variant,prj)importPrim.RowasRowimportType.Proxy (Proxy(..))-- | A container of type `a`dataContainera =Empty|Filledaupdate::forallsarr'.IsSymbols=>Row.Conssarr'=>Containera->Variantr'->ContaineraupdateEmpty v =case prj (Proxy::_s) vofJust y->Filled yNothing->Emptyupdate (Filled x) v =case prj (Proxy::_s) vofJust y->Filled yNothing->Filled x |
timjs commentedJun 1, 2021
Thanks a lot@rhendric! Really appreciate it :-) Now I understand the way to use this. Only thing left is an error onunknown type variables in the instance head of IsSymbol in |
timjs commentedJun 1, 2021
Really didn't know In that case I agree with@hdgarrood that |
You will either need to fix |
rhendric commentedJun 1, 2021 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Yes, exactly. It can be worked around with some type-level fun, as long as everything around it is sufficiently well-typed. Here's a more fully-worked example incorporating your previous moduleMainwhereimportPreludeimportData.Either (Either(..))importData.Maybe (Maybe(..))importData.Symbol (classIsSymbol)importData.Variant (Variant,inj,prj)importEffect (Effect)importPrim.RowasRowimportPrim.RowList (RowList,classRowToList)importPrim.RowListasRowListimportTryPureScript (p,render,text)importType.Data.Boolean (False,True)importType.Proxy (Proxy(..))-- Let's pretend we aren't on try.purescript.orglog::String->EffectUnitlog = text >>> p >>> render-- This is something that should be in a library somewhere (maybe it is?)classFindTypeInRowList ::forallk.Symbol->k->RowListk->Boolean->ConstraintclassFindTypeInRowListsarlb |arl->sbinstanceftirlNil ::FindTypeInRowListsaRowList.NilFalseelse instanceftirlNext ::FindTypeInRowListsa (RowList.Conssarl)Trueelse instanceftirlRec ::FindTypeInRowListsarl'b=>FindTypeInRowListsa (RowList.Conss'a'rl')bdataContainera =Empty|FilledainstanceshowContainer ::Showa=>Show (Containera)where showEmpty ="Empty" show (Filled a) ="(Filled" <> show a <>")"-- This needs a proxy to determine s.update'::forallpsarr'.IsSymbols=>Row.Conssarr'=>ps->Containera->Variantr'->Containeraupdate' pEmpty v =case prj p vofJust y->Filled yNothing->Emptyupdate' p (Filled x) v =case prj p vofJust y->Filled yNothing->Filled x-- This uses type-level shenanigans to determine s, so doesn't require a-- proxy as long as the types are sufficiently determined at the call site.update::forallsarr'rl.RowToListrrl=>IsSymbols=>Row.Conssar'r=>FindTypeInRowListsarlTrue=>Containera->Variantr->Containeraupdate = update' (Proxy::_s)classScanawherescan::String->MaybeainstancescanUnit ::ScanUnitwhere scan =case _of"unit"->Just unit _->NothinginstancescanBool ::ScanBooleanwhere scan =case _of"true"->Justtrue"false"->Justfalse _->Nothing-- purs warns about this _. You could spell out this type entirely as-- parse :: forall r. String -> Either String (Variant (u :: Unit, b :: Boolean | r))-- but it'd be nice if you didn't have to.parse::String->EitherString (Variant_)parse val |Just v<- (scan val::MaybeUnit) =Right $ inj (Proxy::_"u") v |Just v<- (scan val::MaybeBoolean) =Right $ inj (Proxy::_"b") v | otherwise =Left $"error scanning" <> valmain::EffectUnitmain =dolet v::EitherString (Variant (u ::Unit,b ::Boolean)) v = parse"true"let c1::ContainerUnit c1 =Emptylet c2::ContainerBoolean c2 =Filledfalselet result1 = update c1 <$> vlet result2 = update c2 <$> v log (show result1) log (show result2) There are some obvious warts here. The biggest is that we need to provide labels One thing I still don't understand is what |
@rhendric sure - I'm trying to implement (BTW, this ignores the fact that the rendering function itself may change. But that's problem for |
rhendric commentedJun 1, 2021 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
I might not understand that example fully because I don't have the larger context of what else is going on in that MR, but let me try anyway. If you get/require an implementation of dataDependencyFa =DependencyF (StaticPointer (a->a->Boolean))anewtypeDependency =Dependency (ExistsDependencyF)instanceeqDependency ::EqDependencywhere eq (Dependency d1) (Dependency d2) = d1 # runExists \(DependencyF staticEqA a)-> d2 # runExists \(DependencyF staticEqB b)-> staticKey staticEqA == staticKey staticEqB && any (\eq'-> eq' a (unsafeCoerce b)) (derefStaticPtr staticEqA)depending::forallamc.StaticPointer (a->a->Boolean)-> (a->Htmlmc)->a->Htmlmcdepending staticEq f x =Html (\a d b c-> d (Dependency (mkExists (DependencyF staticEq x))) $case f xofHtml h'-> h' a d b c) |
Hmm, very cool idea, but that wouldn't work with something like |
Yeah, you'd want the production of the But if that general idea is sufficient on the consumer side of things, once you have classStaticEqawherestaticEq::StaticPointer (a->a->Boolean)instanceuniversalStaticEqInstance ::Eqa=>StaticEqawhere staticEq = unsafeThrow"not to be used without some fancy CoreFn transformations"depending::forallamc.StaticEqa=> (a->Htmlmc)->a->Htmlmcdepending f x =Html (\a d b c-> d (Dependency (mkExists (DependencyF staticEq x))) $case f xofHtml h'-> h' a d b c) Then rewrite any calls to |
Uh oh!
There was an error while loading.Please reload this page.
Description of the change
Purescript currently doesn't have
Typeable
constraint in style of Haskell, which makes it basically impossible to implement some functionality, e.g. safe dynamically typed values or memoizing in VDOM where memoized nodes change type of argument.Generic
doesn't help there, because it doesn't see difference between constructors defined in different modules, and external libraries don't help either, because they would require users to implement instances for possibly huge amount of types and would prohibit them from using it with external types (because of orphans being dissallowed).This PR implements minimal version of
Typeable
dictionary generation, expecting API to be defined as follows, e.g. inprelude
:(edit: disallowed instances in style of
Coercible
)where
Typeable
gets resolved automatically in style ofIsSymbol
.TypeRep
contains statically generated hash created from normalized string representation of the type (usingGHC.Fingerprint
).I figured that the change is small enough that instead of just talking about it, I can create prototype that can be either dismissed or refined, so I've created PR instead of an issue. Technically, itfixes#2243, though using much more minimal interface as described above, only providing what's needed to implement functionality like
Dynamic
orin some external library.
As far as implementation goes, things I'm not completely sure about are whether it may interact badly with redundant syntax (seems like kind annotations, parens, operators, synonyms and rows passed to
rowToSortedList
get normalized at that point?) and whetherPolyKinds
may create some unexpected problems (they're hashed too by extracting them fromKindApp
right now).When it comes to compatibility guarantees, I would expect same hashes when building using the same compiler version modulo bugfixes - this means that changes to
GHC.Fingeprint
should probably result in minor version change.Checklist: