List.sortBy on record fields of differing types

517 views
Skip to first unread message

Rob Scherf

unread,
Feb 28, 2016, 11:15:06 AM2/28/16
to Elm Discuss
Hi!

I'm trying Elm in a small project: a job posting aggregator. I am trying to use List.sortBy to sort a list of Job records by different fields, which have differing types. 

Using a union type to dispatch the field that sortBy will sort on seems to require that each field will have the same type. Fair enough, but I can't figure out another elegant way to get the behavior I want.

Here's what I mean. Job listings have the following model:


type alias Model = {jobs : Maybe Jobs}
type
alias Jobs = List Job
type
alias Job =
 
{ title : String
 
, organization : String
 
, division : String
 
, urlDetail : String
 
, dateClosing : String
 
, salaryWaged : Bool
 
, salaryAmount : Float
 
}


On the page, jobs are presented as a simple table. You can click any of the table headings to sort the table on that field:


table []
 
(
   
[ tr []
     
[ th [onClick address (SortJobs Title)] [text "Title"]
     
, th [onClick address (SortJobs Organization)] [text "Organization"]
     
, th [onClick address (SortJobs Salary)] [text "Salary/Wage"]
     
, th [onClick address (SortJobs ClosingDate)] [text "Closing Date"]
     
]
   
]
   
-- ... and then the table body where each Job is a row
   
)


SortJobs is an Action wired to the function sortJobs:


sortJobs : SortingCriteria -> Model -> Model
sortJobs criteria model
=
  let currentJobsList
=
       
case model.jobs of
         
Nothing -> []
         
Just js -> js
      sortField
=
       
case criteria of
         
Title -> .title
         
Organization -> .organization
         
Division -> .division
         
Salary -> .salaryAmount
         
ClosingDate -> .dateClosing
      sortedCurrentList
= List.sortBy sortField currentJobsList
 
in
   
if currentJobsList == sortedCurrentList
     
then { model | jobs = Just (List.reverse sortedCurrentList) }
     
else { model | jobs = Just sortedCurrentList }


... and the SortingCriteria union type:


type SortingCriteria
 
= Title -- corresponds to Job.title : String
 
| Organization -- corresponds to Job.organization : String
 
| Division -- corresponds to Job.division : String
 
| Salary -- corresponds to Job.salary : Float
 
| ClosingDate -- corresponds to Job.dateClosing : String


Trying to compile all of this produces the following error:


Detected errors in 1 module.
-- TYPE MISMATCH ----------------------------------------------- src/listing.elm


The type annotation for `sortJobs` does not match its definition.


67 sortJobs : SortingCriteria -> Model -> Model
               
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The type annotation is saying:


   
SortingCriteria
   
-> { jobs : Maybe (List { ..., salaryAmount : Float }) }
   
-> { jobs : Maybe (List { ..., salaryAmount : Float }) }


But I am inferring that the definition has this type:


   
SortingCriteria
   
-> { jobs : Maybe (List { ..., salaryAmount : String }) }
   
-> { jobs : Maybe (List { ..., salaryAmount : String }) }


I think this is saying I can dispatch sortBy on any number of fields, as long as those fields are all the same type.

That's simple to understand (although I'm still a newbie, so the "why" is beyond me). If this is the case, though, I'm stumped about achieving my intended behavior.

Can anyone give me a hint?

Thanks!

Dobes Vandermeer

unread,
Feb 28, 2016, 2:02:37 PM2/28/16
to Elm Discuss
Your "sortField" variable will be a function that returns either a string usually but sometimes a float.  Elm seems to have settled on returning a string being the intention there.  When that is passed to sortBy, it's going to replace "comparable" with "string" and move on.  Unfortunately, Elm's "comparable" type requires special handling and is quickly replaced with a concrete type due to limitations of Elm's type system that exist for various reasons today.

Probably the simplest, although not as concise, solution would be to use List.sortWith, or move the List.sortBy calls into each branch of the case instead of returning the field accessor out.

This might be considered a bug, though.  I'm not sure ... it's kind of a gray area.  I think users would expect the "comparable" feature to propagate farther than it does.


--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Joey Eremondi

unread,
Feb 28, 2016, 2:51:47 PM2/28/16
to elm-d...@googlegroups.com
The TLDR is, in a dynamic language your code wouldn't cause a type error, but getting a statically-checked language to accept it would require a much more sophisticated type-system than Elm has, whose complexity is probably not worth the benefit, for Elm as a language.

In more detail:

I think this is a case where, if we had higher-rank polymorphism and existential types, we'd be able to type a case like this, but the current typesystem isn't able to.

For example, if we had typeclasses and existentials, we could could give "sortField" the type:
    exists c . Comparable c => (Job -> c)
and we could give sortBy the following type:
    forall a b . Comparable b => (a -> b) -> List a -> List a
which we could instantiate to:
    forall b . Comparable b => (Job -> b) -> List Job -> List Job
and could apply the weakening rule for "forall" to get
    exists b . Comparable b => (Job -> b) -> List Job -> List Job

This would then typecheck, and we'd see that we get a List Job as the result, regardless of the type of sortField that we choose.
Reply all
Reply to author
Forward
0 new messages