Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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

Draft
TheMatten wants to merge3 commits intopurescript:master
base:master
Choose a base branch
Loading
fromTheMatten:typeable
Draft

Conversation

TheMatten
Copy link

@TheMattenTheMatten commentedMay 29, 2021
edited
Loading

Description of the change

Purescript currently doesn't haveTypeable 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 ofTypeable dictionary generation, expecting API to be defined as follows, e.g. inprelude:

moduleType.Rep (TypeRep,classTypeable,typeRep,sameTypeRep)whereimportData.Eq ((==))importData.Monoid ((<>))importData.Show (classShow,show)newtypeTypeRepa=TypeRepStringtyperoleTypeRepnominalinstanceshowTypeRep::Show (TypeRepa)whereshow (TypeRep n)="(TypeRep"<> n<>")"classTypeable::forallk.k->ConstraintclassTypeableawheretypeRep::TypeRepasameTypeRep::forallab.TypeRepa->TypeRepb->BooleansameTypeRep (TypeRep a) (TypeRep b)= a== b

(edit: disallowed instances in style ofCoercible)
whereTypeable 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 likeDynamic or

cast::forallab.Typeablea=>Typeableb=>a->Maybeb

in 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 torowToSortedList 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 toGHC.Fingeprint should probably result in minor version change.


Checklist:

  • Added the change to the changelog's "Unreleased" section with a reference to this PR (e.g. "- Made a change (#0000)")
  • Added myself to CONTRIBUTORS.md (if this is my first contribution)
  • Linked any existing issues or proposals that this pull request should close
  • Updated or added relevant documentation
  • Added a test for the contribution (if applicable)

hdgarrood reacted with thumbs down emojitimjs reacted with heart emoji
@rhendric
Copy link
Member

rhendric commentedMay 29, 2021
edited
Loading

I'm reading the conversation in#2243 and, like several of the participants there, I've never had the need forTypeable orDynamic. The thing this most reminds me of, though, isn'tGeneric orForeign as discussed over there, butVariant. Speaking as someone who's never used them, theTypeable/Dynamic approach seems to be about constructing something like a universal sum over every possible type, whereasVariant allows for open but limited disjoint sums. I think this means that you get less boilerplate when working with aDynamic than aVariant, but you also completely decouple your producers and your consumers, such that it's easy to change one side only and get run-time errors that aVariant-based approach would catch.

Is that basically right? If so, my current weakly-held opinion is that I'd rather spend compiler code on making working with rows andVariants less painful, if possible, than on promoting the use ofTypeable. If not, I'd love to hear about a compelling use case forTypeable thatVariant doesn't adequately handle.

@garyb
Copy link
Member

TheVariant comparison seems apt yeah, it's pretty much what Nate was talking about here:#2243 (comment)

I think the big difference though is that variants are tagged with their constructor in the encoding, butTypeable forgoes that even?

@rhendric
Copy link
Member

TheTypeRep in aDynamic seems like it does the same job as the tag in aVariant.

@TheMatten
Copy link
Author

TheMatten commentedMay 29, 2021
edited
Loading

@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,Typeable gets misused from time to time by users which are accustomed to approaches common to dynamically typed languages. But there still are some genuine use-cases for it, mostly when some interface allows for embedding of relatively self-contained parts, like e.g. when creating APIs for caching, serializing or debugging. Plus it's safe - in Purescript, people may be inclined to implement such functionality using hacks in raw JS, but because it isn't possible to distinguishnewtype's at runtime, they can easily introduce bugs by breaking safety guarantees this way. Personally I would rather see code that overusesTypeable, because there's no way to use it unsafely.

@timjs
Copy link

I'd love to haveTypeable in PureScript. I use it a lot in combination withDynamic to parse user input, wrap it, pass it around, and match it against the type of an (Html) input field when needed. Wanted to do this in PureScript, but switched to Haskell in the end. Something along these lines:

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
Copy link
Author

TheMatten commentedMay 30, 2021
edited
Loading

@timjs Yeah, once again you could probably create some top level synonym used in place ofDynamic for variant with all possible types that may appear during parsing, but that would be burdensome and would provide no additional safety.

Really,Variant is in a sense "type-directed generation of local, restrictedTypeable constraints" - so it basically shifts work from the compiler to users, asking them to create "lookup table" of tokens corresponding to specific types at type level. Question is, whether it's worth it to take that work from them - I would say yes, in same way as I would say yes to e.g.IsSymbol.

@timjs
Copy link

timjs commentedJun 1, 2021
edited
Loading

@timjs Yeah, once again you could probably create some top level synonym used in place of Dynamic for variant with all possible types that may appear during parsing, but that would be burdensome and would provide no additional safety.

@TheMatten, that is indeed a possibility, but it assumes aclosed world doesn't it? In my example above, the library in which theinsert function lives, assumes only that a type implementsTypeable. This is an open set of types.

The application, containing theparse function, chooses a particular set of types which it is able to parse. Another application could choose a different set of types. The only restriction by the library is that all types should adhere toTypeable.

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?

@TheMatten
Copy link
Author

@timjs I imagine you should be able to parameterize context you work in with some variant supplied by the user, similarly to how slots inhalogen work. We're technically working with "open" world from programmer's perspective, but the other way of viewing this (which may be closer to what@rhendric is saying) is that in reality you only ever work with finite set of types in your application, and thus you can create someVariant that covers all of them.

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"Variant, I think it's putting extra work and mental overhead on programmer and makes APIs more complicated, just to shave off what seems to amount to roughly 60 lines of code in the compiler.

@hdgarrood
Copy link
Contributor

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 reacted with thumbs up emoji

@TheMatten
Copy link
Author

TheMatten commentedJun 1, 2021
edited
Loading

@hdgarrood Yeah, I should probably be more concrete about maintenance costs:

  • As far as bugfixes of incorrect behaviour go, current design should be minimalistic and opaque enough to limit possibility of backwards-incompatible changes (unless we later find out that e.g. some types can't be safely covered) - basically, it provides no more than optimized equality of normalized types represented as strings, across single compiler version (maybe comparable to equality ofFastStrings in GHC).
  • As far as interaction with other features goes, I think we're enjoying "confluence of design decisions" in this case - compared to GHC,purs comes with much simpler typechecker, doesn't provide features like wired-in type equalities and it'sCoercible doesn't construct any interesting runtime proofs. So it should be actually very easy to use them together - something like
    eqTypeRep::forallabr.TypeRepa->TypeRepb-> (Coercibleab=>r)->Mayber
    can be implemented at library level usingsameTypeRep (andunsafeCoerce) without any support from the compiler. (Edit: and as I understand it, the fact that generation happens after desugaring means that types we're working with are already normalized, making hash generation much simpler.)
  • When it comes to related documentation and guidance around it's use, I would be happy to put it together in caseTypeable gets implemented. Well written comparison withVariant should be enough to make it clear where each of those makes sense in my opinion.

@f-f
Copy link
Member

f-f commentedJun 1, 2021

I am familiar with Variant, but less familiar with Typeable - since here we're talking about implementing a compiler extension, I'm wondering:

  • if implementingTypeable means thatVariant can be implemented on top of it (or viceversa?)
  • would the performance improve? (both of the compiled code, and the compiler)
  • more generally, since it looks like there are two similar usecases here it's worth thinking if it's possible to cover both with the same feature

@TheMatten
Copy link
Author

TheMatten commentedJun 1, 2021
edited
Loading

@f-f To answer your points:

  • One can implement unnamedVariant on top ofTypeable, because it would basically be type-restricted version ofDynamic (that approach is actually common in Haskell, because it's the fastest safe solution). With named ones, you could useTypeReps as additional keys to access different types with same label. But becauseTypeable is "open" whileVariant is "closed", if you were to implement function that extracts whatever field that is already used, you need to cover case of "it's neither of listed options" to be exhaustive from compiler's point of view. On the other hand, you can't really implement "open" variant usingVariant - you can only approximate it by creating global list of types that may appear in variant in your application, as mentioned in the discussion above.
  • Depends on how it's used, but it would be always better or roughly-equal. E.g. if you were to avoidunsafeCoerce in your implementation of variant, you would probably end up with some inductive GADT approximation, which is necessarily going to be slower. Plus, because we don't have actual unnamed variants withoutTypeable (you still need some index, it doesn't matter whether it's position in type-level list found by a typeclass or name inside of row), one has to provide name of field as an argument to every field-specific operation (Proxy). With (unsafely implemented) named variant though, representation itself would be basically the same, except for length of keys.
  • I would actually say that given the description above, their usecases are actually sligthly different:
    • Variant works better with APIs oriented around extraction, because of explicit labels. It may be better fit for cases where concrete identity of selection matters instead of it's type, like e.g. components in currenthalogen
    • Typeable works better with APIs oriented around injection, where difference between selections matters mostly from implementation perspective, or where type itself is enough to distinguish purpose of the value. This is the case withShpadoinkle, which is merely interested in memoization of anonymous nodes.

@hdgarrood
Copy link
Contributor

I'm 👎 because I don't think this provides enough utility/is in enough demand to justify adding a feature to the compiler, sorry.

@TheMatten
Copy link
Author

@hdgarrood no problem 🙂 Alternatively, would it be possible to expose needed information e.g. in#4092?

@hdgarrood
Copy link
Contributor

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.

TheMatten reacted with thumbs up emoji

@timjs
Copy link

Sorry, I've lost the discussion onTypeable vsVariant a bit.

To make it concrete. Say I've some container type, which is empty or filled with a value of typea. Given aDynamic, I'd like to update the value inside the container, but obviously only when the types match.

{-#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 aVariant of some row to model the same thing. Hope somebody can help me understand this.

@rhendric
Copy link
Member

rhendric commentedJun 1, 2021
edited
Loading

@timjs

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 and paluh reacted with heart emoji

@timjs
Copy link

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 inupdate when using this solution. Any ideas?

@timjs
Copy link

Really didn't knowVariant was so powerful :-O

In that case I agree with@hdgarrood thatTypeable is not a necessary extension we'd need to implement and maintain. Maybe focussing on the ergonomics ofVariant would be a better idea!

@TheMatten
Copy link
Author

Only thing left is an error on unknown type variables in the instance head of IsSymbol in update when using this solution. Any ideas?

You will either need to fixs into something specifc, or pass inProxy s that provides it as an argument.

@rhendric
Copy link
Member

rhendric commentedJun 1, 2021
edited
Loading

You will either need to fixs into something specifc, or pass inProxy s that provides it as an argument.

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 previousparse example as well:

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 labelsu andb in our row type. I could imagine a version ofVariant which usesHLists instead of rows, and then the tag could be provided by the equivalent ofFindTypeInRowList—this could be written with today's compiler, to my knowledge. Other issues mostly involve type inference and making it easier to elide the full row types without generating warnings. I think improving all of these could be better options than implementingTypeable.

One thing I still don't understand is whatTypeable can do for memoization.@TheMatten, can you point me to a small example of what you'd want that for? I'm not familiar with Shpadoinkle, unfortunately.

paluh and timjs reacted with heart emoji

@TheMatten
Copy link
Author

@rhendric sure - I'm trying to implementdepending combinator:https://gitlab.com/platonic/shpadoinkle/-/merge_requests/182/diffs#927128ccef388486a73f825b27dde5f95305bc72_325_348
Basically, what it does is that it storesTypeable argument alongside the rendering function of a memoized node - this way, you can try to cast old argument into type of new one to compare them for equality, or rerender if either of those actions fails. I don't really see a way to implement this withVariant without affectingHtml type, but I don't think this should appear in it in the first place - it's an optimization detail, not a safety/corectness concern.

(BTW, this ignores the fact that the rendering function itself may change. But that's problem forStaticPointers, which should be implementable withpurs codegen.)

@rhendric
Copy link
Member

rhendric commentedJun 1, 2021
edited
Loading

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 ofStaticPointer, is that enough?

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)

@TheMatten
Copy link
Author

Hmm, very cool idea, but that wouldn't work with something like\_ _ -> true orunsafeRefEq, where the function could be used with multiple nodes, would it?

@rhendric
Copy link
Member

Yeah, you'd want the production of theStaticPointer to be safe.

But if that general idea is sufficient on the consumer side of things, once you havepurs codegen you could make the user side much more foolproof. Just:

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 touniversalStaticEqInstance(...) in CoreFn toStaticEq(static(Data_Eq.eq(...))) prior to running theStaticPointer transformation. If theStaticPointer transformation succeeds, you got safe pointers to unique equality functions. If theEq instance contains any non-staticable subexpressions, the transformation fails (and the user has to move theStaticEq constraint around probably).

TheMatten reacted with thumbs up emoji

Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers
No reviews
Assignees
No one assigned
Labels
None yet
Projects
None yet
Milestone
No milestone
Development

Successfully merging this pull request may close these issues.

Automatically generate instances for Data.Typeable
6 participants
@TheMatten@rhendric@garyb@timjs@hdgarrood@f-f

[8]ページ先頭

©2009-2025 Movatter.jp