Trying to write some reusable logic for sortable tables in Halogen (with Heterogeneous)


#1

Hi!
I’ve noticed a pretty common (and a bit tedious) pattern in my tables: I have an array of records, and I want to be able to sort them by each field (without any kind of async server interaction).

Right now I’m doing this in a very clumsy way, by defining a sum type SortBy, and then using it to to build the table header, as an argument in the sort function, etc., but I’d like to be able to derive most of this stuff from types.

I got somewhere, but I need some directions, or maybe just to know if I’m on the right path and if this is possible.

This is what I have right now:

data SortOrder = Asc | Desc
derive instance eqSortOrder :: Eq SortOrder

flipSort :: SortOrder -> SortOrder
flipSort Asc = Desc
flipSort Desc = Asc

type SortBy sym = SProxy sym

data ArraySort a = ArraySort (SortBy a) SortOrder

sortResults ::
  forall sym a r' r.
  IsSymbol sym =>
  Row.Cons sym a r' r =>
  Ord a =>
  ArraySort sym ->
  Array { | r } ->
  Array { | r }
sortResults (ArraySort sB sO) elems =
  let res = sortWith (Record.get sB) elems
  in case sO of
    Asc -> res
    Desc -> reverse res

data BuildM fn = BuildM fn

instance buildM_ ::
  (Monoid m, IsSymbol sym) =>
  FoldingWithIndex (BuildM (a -> m)) (SProxy sym) m a m where
  foldingWithIndex (BuildM fn) prop arr a = fn a <> arr

buildM ::
  forall a m r.
  Monoid m =>
  HFoldlWithIndex (BuildM (a -> m)) m { | r } m =>
  (a -> m) ->
  { | r } ->
  m
buildM fn r = hfoldlWithIndex (BuildM fn) mempty r

I have a function to sort my array given a SProxy and a tentative function that when complete will build Halogen HTML.

The problem is that this function needs to have access to the SProxy too, both to render the label and to call sortResults with the correct value.
If I change (a -> m) to (SProxy sym -> a -> m) it works, but I’m only allowed to have one label in my record obviously :smiley:

What can I do? Thanks!


#2

@dariooddenino, the sortResults function looks like a good start, but I don’t understand its relation to the buildM function. From what I see, the buildM function folds a record by accumulating its values in an Array, no? Do you intend to evolve buildM into a function which generates Halogen markup?

I can’t clearly see the exact problem you’re trying to solve. So… I’ll just write some pseudocode which might help you. I’m presuming the Monoid is the Halogen markup you mention above.


data SortOrder = Asc | Desc
derive instance eqSortOrder :: Eq SortOrder

data SortConfig fieldName = SortConfig (SProxy fieldName) SortOrder

sortResults ::
  Row.Cons field val r' r =>
  Ord val =>
  IsSymbol field =>
  SortConfig field ->
  Array (Record r) ->
  Array (Record r)
sortResults (SortConfig fieldName order) recs =
  case order of
    Asc -> sortedRecs
    Desc -> reverse sortedRecs
  where
    sortedRecs = sortWith (Record.get (reflectSymbol fieldName)) recs


-- A record fold instance which creates a table header from a record.
data TableHeader = TableHeader

instance tableHeaderFolding ::
  (Monoid m, IsSymbol sym) =>
  FoldingWithIndex TableHeader (SProxy sym) m a m where
  foldingWithIndex TableHeader prop accum _ =
    (tableHeaderElement (reflectSymbol prop))

-- A record fold instance which creates a table row from a record, 
-- if the record's values have a render function defined (RenderRowVal).
data TableRow = TableRow

instance tableRowFolding ::
  (Monoid m, IsSymbol sym, RenderRowVal a m) =>
  FoldingWithIndex TableRow (SProxy sym) m a m where
  foldingWithIndex TableRow _ accum val =
    (tableRowElement (renderRowVal val))


recsToTable ::
  Monoid m =>
  HFoldlWithIndex TableHeader m (Record r) m =>
  HFoldlWithIndex TableRow m (Record r) m =>
  Array (Record r) ->
  m
recsToMarkup recs = tableElement (tableHeader <> tableRows)
  where
    tableHeader :: m
    -- Just need one record for `foldingWithIndex` to get the type info.
    tableHeader = foldingWithIndex TableHeader mempty (unsafePartial head recs)
    tableRows :: m
    tableRows = foldMap recToRow recs
    recToRow :: Record r -> m
    recToRow rec = foldingWithIndex TableRow mempty rec

sortAndRenderTable ::
  Monoid m =>
  SortConfig ->
  Array (Record r) ->
  m
sortAndRenderTable sortConfig recs = recsToMarkup (sortResults sortConfig recs)

#3

Thanks for answering and sorry for net being enough clear!

The two functions are not directly related; I just want to use them together with the goal of handling my tables.

Yes, I wanted buildM to eventually become a function that could build Halogen markup, given a function fn :: (SProxy sym -> a -> HH.HTML) and a Record, so that I could have used it to build both header and rows cells.

The problem with my approach is that, as I wrote in the other message, it works only for records with one label. I’m still on shaky grounds when doing type level programming, so I never know if I’m trying to do something impossibile or if I’m just using the wrong approach.

I will check your solution once I’m back at the office, thanks again! :slight_smile: