[Haskell-cafe] Runtime selection on types while capturing a dictionary?

Benjamin Redelings benjamin.redelings at gmail.com
Mon Oct 14 14:19:56 UTC 2024


On 10/9/24 10:11 AM, Benjamin Redelings wrote:
> Interesting! And thanks.  I've been able to move the labels into the 
> Graph class, and just use () as the label type when I don't want 
> labels.  Somehow I was expecting a Haskell mailing list to recommend 
> something involving DataKinds.
>
> The other augmentations are a bit more tricky.  The tree can have 
> either a BranchLength augmentation OR a NodeTime augmentation, but not 
> both.  The NodeTime augmentation only makes sense when the tree is 
> rooted.  Additionally, it makes sense on rooted forests and directed 
> acyclic graphs.  When using the NodeTime augmentation, we can 
> CALCULATE the branch lengths as the time difference between the nodes 
> at the two ends of a branch.  This provides some motivation to make 
> the branchLength function polymorphic, so that some of the downstream 
> code can be agnostic as to whether the branch lengths are directly 
> augmented, or computed from NodeTimes.  And then there is also a 
> BranchRate augmentation that only makes sense if we have a NodeTime 
> augmentation.
>
> I think your ADT makes more sense than just creating a lot of 
> (AddAugmentationX ExtraInfo Graph) data types.  So, I'll have to think 
> how to do that.
>
> On the other hand, I would like to retain some polymorphism.  For 
> example, it would be nice to allow multiple tree representations. 
> Sometimes a generic graph is what you want, but in other cases the 
> tree-generation process makes a data structure like
>
>     data Tree = Leaf {parent :: Maybe Tree } | Internal { left:: Tree, 
> right:: Tree, parent:: (Maybe Tree)}
>
> more natural.  So, having an IsGraph *class* versus just a single 
> Graph data structure seems valuable.
>
> QUESTION: Given that, can you expand on what counts as a type-class 
> "shenanigan"?  I fully agree that the class structure and the data 
> type structure isn't very good yet.  (It was implemented in haste to 
> make something "just work".  The standard format for printing trees -- 
> Newick format -- really prints rooted trees, so I ended up creating a 
> 'type family Rooted t' to make a rooted tree type from an unrooted 
> tree type.  That seems very hacky.)  But I would like to retain some 
> generality across common features between NodeTime and BranchLength 
> augmented trees, as well as (perhaps) generality across different 
> implementations of Graph / Tree.
>
> QUESTION: For the constraint that a NodeTime tree should be rooted, do 
> you think I should encode this constraint at the data-type level, or 
> at the function level?  I was initially thinking of encoding this at 
> the data type level, so that you couldn't construct a tree with node 
> times that was not rooted. But I suppose one could simply make all the 
> functions that USE node-time trees require some kind of Rooted 
> constraint by calling a function like 'parent node'.  This would 
> decouple the Rooted/Unrooted augmentation from the BranchLength / 
> NodeTime / Nothing augmentation.  Thoughts?
>
> -BenRI
>
>
> On 10/7/24 6:13 PM, Tom Ellis wrote:
>> On Mon, Oct 07, 2024 at 05:38:30PM -0400, Benjamin Redelings wrote:
>>> I'm new to Haskell, and I'm writing a class for evolutionary trees.  
>>> Since
>>> I'm new to Haskell, trying to encode information at the type level 
>>> is new to
>>> me.  In C++ I'd use runtime checks to ask whether (for example) a 
>>> tree is
>>> rooted.  Or I'd have the getRoot function return Maybe NodeID and 
>>> then make
>>> the methods for checking if a branch points rootward throw an 
>>> exception if
>>> the tree is unrooted.
>> I think I'd just do
>>
>>      data AugmentedTree roots lengths labels =
>>        MkAugmentedTree {
>>          tree :: Tree,
>>          labels :: labels,
>>          roots :: roots,
>>          lengths :: lenghts
>>        }
>>
>> and then just do stuff like
>>
>>      type FullyAugmentedTree =
>>          AugmentedTree [NodeId] (IntMap Double) (IntMap (Maybe l))
>>        type PartiallyAugmentedTree =
>>          AugmentedTree [NodeId] () (IntMap (Maybe l))
>>
>> For rootedness-independence I strongly recommend against any type
>> class shenanigans.  Instead you can do
>>
>>      data PossiblyRootedTree lengths labels where
>>          RootedTree :: AugmentedTree [NodeId] lengths labels
>>          UnrootedTree :: AugmentedTree () lengths labels
>>
>> and then
>>
>>      isRooted :: PossiblyRootedTree lengths labels -> Bool
>>      isRooted = \case
>>         RootedTree {} -> True
>>         UnrootedTree {} -> False
>>
>> (but `isRooted` is not really very useful compared to just pattern
>> matching every where you need to know.)
>>
>> Tom
>> _______________________________________________
>> Haskell-Cafe mailing list
>> To (un)subscribe, modify options or view archives go to:
>> http://mail.haskell.org/cgi-bin/mailman/listinfo/haskell-cafe
>> Only members subscribed via the mailman list are allowed to post.


More information about the Haskell-Cafe mailing list