purescript-simple-dom icon indicating copy to clipboard operation
purescript-simple-dom copied to clipboard

Disentangle inheritance (Node/Element/Document/HTML*)?

Open dylex opened this issue 10 years ago • 4 comments

Currently, HTMLDocument is an instance of Element, which isn't actually true in the DOM (attribute and class-related functions are not on Document), but Element also contains some methods actually on Node, which are on Document (children, tree stuff, though actually these are on ParentNode). Furthermore, some things are actually on HTMLElement or HTMLDocument but not Element or Document. This makes it confusing to know where to put new methods.

Obviously this is a hard problem to solve, not only because different browsers implement these things slightly differently, but mostly because many of the types in the DOM are determined at run-time (especially HTML*, ParentNode).

Having type classes to represent these things is nice. Maybe there just need to be some more classes to represent Node, etc? This doesn't solve the runtime problem, though. Another option would be lots of newtypes, which could be dynamically cast up and down, but this doesn't solve the multiple-inheritance problem (though there's not too much of this).

I don't have a concrete suggestion, and do like how simple everything is now... Mainly looking for guidance for submitting more PRs. As mentioned on #4, a lot of this is just a naming problem -- maybe if there were just a system for parallel type class and concrete type naming (instance FooClass Foo)?

dylex avatar Nov 02 '14 00:11 dylex

Browsers handling things differently could be fun, at the moment i'm using the library on iOS (so i've only really tested it on webkit). Not sure on an easy solution around the differences (outside of a vendor prefixing like setup, which i'd really like to avoid). Maybe something like choosing one platform (i'd say webkit) as the primary and having additional functions for the differences found in others.

I do like the idea of using newtypes and dynamically casting, we could ensure runtime safety (although not sure on the performance cost) with if (typeof .. ). Though I'm assuming we're also going to end up with quite a lot of tryCastXYZ like functions (ie tryCastHTMLAnchor :: forall a. (HTMLElement a) => a -> Maybe HTMLAnchorElement) though I guess theres no easy way around that.

aktowns avatar Nov 06 '14 16:11 aktowns

There might not be all that many down casts -- like how your approach to events manages to avoid them -- but yeah they're unavoidable in some cases. It's the up casts that may be more problematic. With newtype wrappers the performance overhead is minimal, but the user has to explicitly convert an HTMLAnchorElement back up to a Node (through like 3 levels) to do parentNode. With type classes, the user doesn't need to do anything, but there's a bit more performance penalty (though maybe not much if the instances just contain a "coerce" type function to the concrete type that is just identity).

dylex avatar Nov 18 '14 15:11 dylex

Something to be aware of is that the performance costs today should be higher than in the future. There's still quite a bit of optimization the compiler has left in it, so type classes might be quite a bit more efficient later on. I know right now they make the compiled output a bit slower than other methods, but is it that much of a performance penalty right now?

For another suggestion, what about making use of extensible records and type synonyms? Since everything is an object on the js side, you can view them all as records on the ps side. Then instead of having opaque data types like foreign import data HTMLElement :: * and using type classes to show it's an element, you could have a type synonym for element and htmlelement:

type Element r =
  { children :: ...
  , innerHTML :: ...
  , ...
  | r
  }

type HTMLElement r = Element
  ( contentEditable :: ...
  , dir :: ...
  , ...
  | r
  )

or whatever. In this example, HTMLElement is just a record that has Element fields, and also some more fields. If you have a function which gives an HTMLElement, just have it return a record with all the fields HTMLElement has.

So, you end up still able to write functions that take or return Element or Node or whatever at a high level, but you don't have to worry about explicit conversions, and you keep the runtime penalty low. Then it all comes down to {co,contra}variance of the records.

Of course, I'm not all that familiar with the inheritance hierarchy, so I'm not sure if it's really possible.

joneshf avatar Nov 18 '14 16:11 joneshf

@joneshf using that approach, how could you handle cycles in type synonyms? For example, I'd like to do:

type EventTarget r =
  { addEventListener :: Fn2 String { handleEvent :: Fn1 (Event {}) Unit } Unit
  | r
  }

type Event r =
  { target :: Null (EventTarget {})
  | r
  }

but this causes a cycle in type synonyms, which is a compile error.

hdgarrood avatar Mar 15 '15 04:03 hdgarrood