DEV Community

Catherine Galkina for Typeable

Posted on • Originally published at


Comparing Elm with Reflex

Author: Volodya Kalnitsky


This post deals with two essentially different approaches to reactive programming.

Elm, unlike Reflex, is a separate language, not a library, which is why it’s not quite correct to compare them. Nevertheless, it is possible to show the difference between the approaches and describe the practical challenges you may face when developing software using each of the technologies.

Elm and TEA

Elm is a functional programming language used to create reactive web applications. Elm applications must follow The Elm Architecture (TEA), i.e. a simple design pattern implying the code division into three parts – model, view and update:

  • Model includes the data used in the application, as well as the data describing the messages needed for any interaction.
  • View is the function transforming the model into the user interface.
  • Update is the function responsible for the status update (it receives the model and message and returns the updated model).

FRP in a Reflex style

Reflex is the framework that allows creating reactive web applications in Haskell.

In contrast to Elm, Reflex doesn’t impose any strict constraints on the application architecture. The framework provides abstractions to control the state and the programmer is free to use them in any combination.

There are three main abstractions, namely Event, Behavior and Dynamic.


Event is an abstraction used to describe discrete events occurring from time to time instantaneously. The events are parametrized by the type of the value they contain.


Behavior can be regarded as a changing value which can be sampled at any point in time. However, it is not possible to “subscribe” to the value update.

Dynamic = Event + Behavior

Conceptually, Dynamic is the pair consisting of the Event and Behavior.

You can obtain each of the Dynamic components using the pure functions updated and current:

current ::  => Dynamic t a -> Behavior t a
updated ::  => Dynamic t a -> Event t a
Enter fullscreen mode Exit fullscreen mode

(Here and elsewhere, the parameter of t type can be ignored – this is an implementation feature. For the sake of legibility, the constraints in type signatures here and in some places below are replaced with ellipses).

Dynamic guarantees that the following invariants hold:

  • After the event is launched, the behavior changes its own value to a value from the Event.
  • Each change in the behavior is preceded by an event launching.

It’s not possible to create an “incorrect” Dynamic using the library functions.

Dynamic is convenient for the programmer because it rules out the errors relating to any status updates missed. All events potentially affecting the Dynamic must be explicitly listed during its creation, which is why the errors relating to an accidental status update (for example, from another part of the program) are also eliminated.

It’s important to note that the Dynamic can be “updated” with the same value it had before. If the values in the Dynamic determine a DOM section, it will be rebuilt because the framework is not able to test random values for equality (further in the text we will show how this can be avoided).

Code Examples

We will consider two implementations of an interactive questionnaire widget based on the two technologies. The users see a list of questions with the answer options known in advance. The users select one answer for each question from these options and can compare their answers with the correct ones after clicking on the check button.

The check button must not be available until an answer is selected for each of the questions. After the check, we must show the score, i.e. the number of correct answers.

To find the complete code of both applications follow the link. (For convenience, the links to the main files are Elm, Reflex).

For the Reflex example, we use the Obelisk framework. It allows implementing both the frontend and the backend and is also responsible for the server-side rendering and routing. We will use it as the build system only (we have no backend here).

Description of the Application State

Like Haskell, Elm supports algebraic data types. The code below declares the data types we need to create our application (Model). Similar declarations in Haskell are not conceptually different in any way, so we will not show them.


type alias QuestionText = String

type alias AnswerText = String

type IsChosen = Chosen | NotChosen

type IsCorrect = Correct | Incorrect

type CanCheckAnswers = CanCheckAnswers | CantCheckAnswers

type AreAnswersShown = AnswersShown | AnswersHidden

type alias Answer =
  { answerText : AnswerText
  , isCorrect : IsCorrect

type Score
  = NoScore
  | Score { totalQuestions : Int, correctAnswers : Int }

type alias Questions = List (QuestionText, List (Answer, IsChosen))

type alias Model =
  { areAnswersShown : AreAnswersShown
  , allQuestions : Questions
  , canCheckAnswers : CanCheckAnswers
  , score : Score
Enter fullscreen mode Exit fullscreen mode

Message/Event Description


The type describing an Elm message contains all the values we may need to update the status. We will use two indices to select an answer option: the question number and the number of the answer to this question. For the sake of simplicity, we also store the new value IsChosen in the event payload.

type Msg
  = SelectAnswer
  { questionNumber : Int
  , answerNumber : Int
  , isChosen : IsChosen }
  | CheckAnswers
Enter fullscreen mode Exit fullscreen mode


In Reflex, we deal with several independent event values, that’s why we use an individual type to describe each of them:

-- | Answer option selection.
data SelectAnswer
  = SelectAnswer
  { questionNumber :: Int
  , answerNumber :: Int }

-- | Payload for the event "show answers"
data CheckAnswers = CheckAnswers
Enter fullscreen mode Exit fullscreen mode

General Architecture


Much has been said about the architecture of Elm applications — dozens of posts on the topic are available. However, we don’t use Elm at Typeable, so we cannot share our experience in this area but provide the reader with references to other sources:


Despite the fact that Reflex doesn’t impose strict constraints on the architecture, it makes sense to follow some agreements on the code organization.

Our experience with Reflex has resulted in a certain pattern we will describe further.

Let’s start with general considerations.

It’s rather convenient to separate the application representation (interface) and internal logic. But what exactly does the representation mean? Conceptually, the representation can be defined as the function that takes on the widget status (in case of Reflex, it’s dynamic, as it’s fully or partially “wrapped” in Dynamic) and returns an interface description and multiple events occurring during interaction with the interface.

The monad constrained by the DomBuilder is responsible for the “interface description”. Here and elsewhere we use ObeliskWidget constraint (from Obelisk) that includes the DomBuilder.

Thus, in a general case, the representation description in the form of a Haskell function could have the following type (for now, we don’t specify the type variables events and state):

ui :: ObeliskWidget js t route m => state -> m events
Enter fullscreen mode Exit fullscreen mode

The application logic will be implemented in another function, which, on the contrary, takes on multiple events and returns the dynamic status:

model :: ObeliskWidget js t route m => events -> m state
Enter fullscreen mode Exit fullscreen mode

To use them together, let’s declare an auxiliary high-order function, mkWidget:

mkWidget :: ObeliskWidget js t route m
  => (events -> m state) -> (state -> m events) -> m ()
mkWidget model ui  = void (mfix (model >=> ui))
Enter fullscreen mode Exit fullscreen mode

We used function mfix (a combinator of the fixed point for the monadic computation) with the type MonadFixm => (a -> m a) -> m a, as well as a combination of Kleisli arrows sometimes called just a fish:

(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> (a -> m c)`
Enter fullscreen mode Exit fullscreen mode

The division into UI and the “model” turns out to be rather convenient when you write dynamic widgets. You may notice the similarity between UI and the view in Elm, as well as between the “model” and the update. The difference is how exactly we transfer the status and the events.

Let's get back to our widget.

Let’s specify the state and events variables for our questionnaire widget.

According to the specification, there are only two events: the answer selection and clicking on the check button:

data QuizEvents t = QuizEvents
  { selectAnswer :: Event t SelectAnswer
  , showAnswers :: Event t CheckAnswers }
Enter fullscreen mode Exit fullscreen mode

The structure containing the dynamic data will be somewhat more complex:

data QuizState t = QuizState
  { areAnswersShown :: Dynamic t AreAnswersShown
  , allQuestions :: [(QuestionText, [(Answer, Dynamic t IsChosen)])]
  , canCheckAnswers :: Dynamic t CanCheckAnswers
  , score :: Dynamic t Score }
Enter fullscreen mode Exit fullscreen mode

We “wrap” in Dynamic only the status parts which may change. In particular, allQuestions field has only the value of IsChosen type “wrapped” in Dynamic. Of course, using a single Dynamic would allow writing a simpler code. But in this case, we would have had to rebuild even the static DOM parts.

This is another important difference between the two frameworks — in Reflex we control the DOM update ourselves, while the VDOM implementation built into Elm does this for us.

Status Update


Function update receives the message and the previous state and returns the new one:

update : Msg -> Model -> Model
update msg = case msg of
  SelectAnswer { questionNumber, answerNumber, isChosen } ->
    updateCanCheckAnswers <<
    ( mapAllQuestions
      <| updateAt questionNumber
      <| Tuple.mapSecond
      <| updateAnswers answerNumber isChosen )
  CheckAnswers -> mapAnswersShown (\_ -> AnswersShown) >> updateScore
Enter fullscreen mode Exit fullscreen mode

We use auxiliary functions and the function composition operator (>>) to perform the status update in parts.

The rather awkward syntax used to update the fields of record types in Elm makes it necessary to declare manually such functions as mapAnswersShown, mapAllQuestions and the like, which just apply the function to the field value. In Haskell, we could use the lenses (refer to the lens package and its analogs) generated automatically for each data type.

updateCanCheckAnswers : Model -> Model
updateCanCheckAnswers model =
  { model | canCheckAnswers =
    if List.all hasChosenAnswer model.allQuestions
    then CanCheckAnswers
    else CantCheckAnswers }

updateScore : Model -> Model
updateScore model =
    hasCorrectAnswer (_, answers) =
      List.any isCorrectAnswerChosen answers
    correctAnswers =
      List.length <| List.filter hasCorrectAnswer model.allQuestions
    totalQuestions = List.length model.allQuestions
    { model | score =
      Score { correctAnswers = correctAnswers
            , totalQuestions = totalQuestions } }

updateAnswers : Int -> IsChosen -> List (Answer, IsChosen) -> List (Answer, IsChosen)
updateAnswers answerIx newIsChosen =
  List.indexedMap <| \aix ->
    Tuple.mapSecond <| \isChosen ->
      if aix /= answerIx
        if newIsChosen == Chosen
        then NotChosen
        else isChosen
      else newIsChosen
Enter fullscreen mode Exit fullscreen mode


We form the dynamic status by receiving the widget events at the input and then saving all dynamic data in the fields of the returned QuizState value:

mkQuizModel :: ObeliskWidget js t route m
  => [(QuestionText, [Answer])]
  -- ^ List of questions with answers
  -> QuizEvents t
  -> m (QuizState t)
mkQuizModel questions events = do
  areAnswersShown <- holdDyn AnswersHidden (showAnswers events $> AnswersShown)
  allQuestions <- mkAllQuestionsModel questions events
  canCheckAnswers <- mkCanCheckAnswersModel allQuestions
  score <- mkScoreModel areAnswersShown allQuestions
  return QuizState{..}
Enter fullscreen mode Exit fullscreen mode

To this end, we use several combinators from Reflex.Dynamic module.

holdDyn :: MonadHold t m => a -> Event t a -> m (Dynamic t a)
Enter fullscreen mode Exit fullscreen mode

holdDyn is probably the easiest way of creating the Dynamic from the Event. Each time an event occurs, the values in the obtained Dynamic changes to the one contained in the event. At the same time, until the first event takes place, the Dynamic will contain the value we passed as the first argument.

Thus, dynamic value areAnswersShown will include the AnswersHidden until the event stored in field showAnswers occurs, and after that, it will change to AnswersShown.

In the auxiliary function mkAllQuestionsModel we perform iteration by the enumerated list of questions, and in the internal cycle – by the list of answers to each of them, in order to provide each of them with the dynamic status with IsChosen type:

mkAllQuestionsModel :: ObeliskWidget js t route m
  => [(QuestionText, [Answer])]
  -- ^ List of questions with answers
  -> QuizEvents t
  -> m [(QuestionText, [(Answer, Dynamic t IsChosen)])]
mkAllQuestionsModel questions events = do
  for (enumerate questions)
    \(qNum, (questionText, answers)) -> do
    (questionText, ) <$> for (enumerate answers)
      \(aNum, Answer{..}) -> do
        updChosenState SelectAnswer{questionNumber,answerNumber} isChosen = do
        guard (questionNumber == qNum)
          if answerNumber == aNum
          then toggleChosen isChosen
          else NotChosen
      isChosenDyn <- foldDynMaybe updChosenState NotChosen
        (selectAnswer events)
      return (Answer{..}, isChosenDyn)
Enter fullscreen mode Exit fullscreen mode

Expression (questionText,) is the syntax sugar for (\x -> (questionText, x)).

Combinator foldDynMaybe allows updating the Dynamic considering its previous status, as well as the event payload. Maybe allows omitting the update, if it is not required:

foldDynMaybe :: (Reflex t, MonadHold t m, MonadFix m) => (a -> b -> Maybe b) -> b -> Event t a -> m (Dynamic t b)
Enter fullscreen mode Exit fullscreen mode

Expression foldDynMaybe updChosenState NotChosen (selectAnswer events) creates the Dynamic that changes between the two values: Chosen and NotChosen in case a click on the answer option occurs.

The function used to update the status looks as follows:

updChosenState SelectAnswer{questionNumber,answerNumber} isChosen = do
  guard (questionNumber == qNum)
    if answerNumber == aNum
    then toggleChosen isChosen
    else NotChosen
Enter fullscreen mode Exit fullscreen mode

We used guard :: Alternative f => Bool -> f () to return Nothing (by doing so, we omit the Dynamic update — let’s remind that Maybe has the Alternative instance) in case the event is not related to the current question. Otherwise, we either change over value IsChosen or set it to NotChosen, if the other answer option was clicked. In this way, it is possible to select only one answer.

Further, in function mkCanCheckAnswersModel we form the dynamic value CanCheckAnswers that takes on value CanCheckAnswers only when every question has a selected answer (otherwise, it will be CantCheckAnswers):

mkCanCheckAnswersModel :: ObeliskWidget js t route m
  => [(QuestionText, [(Answer, Dynamic t IsChosen)])]
  -> m (Dynamic t CanCheckAnswers)
mkCanCheckAnswersModel allQuestions = holdUniqDyn do
  -- At least one answer was selected for each question
  allQuestionsAnswered <- all (Chosen `elem`) <$> do
    for allQuestions \(_, answers) -> do
     for answers \(_, dynIsChosen) -> dynIsChosen
  return if allQuestionsAnswered then CanCheckAnswers else CantCheckAnswers
Enter fullscreen mode Exit fullscreen mode

Dynamic is in the Monad type class, which is why we can use the do notation.

It has to be noted that we don’t want to update canCheckAnswers value each time we choose an answer as this would cause a useless DOM rebuilding. We are interested only in the updates which really change the value. This is why we use holdUniqDyn to deal with “unnecessary” updates:

holdUniqDyn :: (Eq a, ...) => Dynamic t a -> m (Dynamic t a)
Enter fullscreen mode Exit fullscreen mode

Constraint Eq a means that the equality check function must be defined for type a.

Similarly, we form the dynamic value Score that contains NoScore value, if the results have not been counted yet, or the results, in the opposite case.

mkScoreModel :: ObeliskWidget js t route m
  => Dynamic t AreAnswersShown
  -> [(QuestionText, [(Answer, Dynamic t IsChosen)])]
  -> m (Dynamic t Score)
mkScoreModel areAnswersShown allQuestions = holdUniqDyn do
  areAnswersShown >>= \case
    AnswersHidden -> return NoScore
    AnswersShown -> do
    correctAnswers <- flip execStateT 0 do
      for_ allQuestions \(_, answers) -> do
      for_ answers \(answer, dynIsChosen) -> do
        isChosen <- lift dynIsChosen
        when (isChosen == Chosen && isCorrect answer == Correct) do
        modify (+ 1)
    return Score { correctAnswers, totalQuestions = length allQuestions }
Enter fullscreen mode Exit fullscreen mode

Using the monad transformer StateT allows describing the process of counting correct answers in a more “imperative” way.



In Elm, rendering is rather obvious and needs no detailed explanation.

view : Model -> Html Msg
view model =
  div [] <|
  List.indexedMap (viewQuestion model.areAnswersShown) model.allQuestions ++
  [ div [ class "check-answers-button-container" ] [ viewFooter model ] ]

viewQuestion : AreAnswersShown -> Int -> (QuestionText, List (Answer, IsChosen)) -> Html Msg
viewQuestion areShown questionIx (question, answers) =
  div [class "question"] <|
    [ text question ] ++
    [ div [class "answers"]
    <| List.indexedMap (viewAnswer areShown questionIx) answers ]

viewAnswer : AreAnswersShown -> Int -> Int -> (Answer, IsChosen) -> Html Msg
viewAnswer areShown questionIx answerIx (answer, isChosen) =
    events = [ onClick <|
               SelectAnswer { questionNumber = questionIx
                            , answerNumber = answerIx
                            , isChosen = toggleChosen isChosen
    className = String.join " " <|
      ["answer"] ++
      ( if isChosen == Chosen
      then ["answer-chosen"]
      else [] ) ++
      ( if areShown == AnswersShown
        then ["answer-shown"]
        else ["answer-hidden"] ) ++
      ( if answer.isCorrect == Correct
        then ["answer-correct"]
        else ["answer-incorrect"] )
    attrs = [ class className ]
  div (attrs ++ events) [ text answer.answerText ]

viewFooter : Model -> Html Msg
viewFooter model =
  case model.score of
    NoScore ->
       case model.canCheckAnswers of
         CanCheckAnswers ->
           button [ onClick CheckAnswers ] [ text "Check answers" ]
         CantCheckAnswers ->
           div [ class "unfinished-quiz-notice" ]
               [ text "Select answers for all questions before you can get the results." ]
    Score { totalQuestions, correctAnswers } ->
      text <|
        "Your score: " ++ String.fromInt correctAnswers ++
        " of " ++ String.fromInt totalQuestions
Enter fullscreen mode Exit fullscreen mode

We have fully described the widget. Now let’s get down to its initialization:

initialModel =
  { areAnswersShown = AnswersHidden
  , allQuestions = allQuestions
  , score = NoScore
  , canCheckAnswers = CantCheckAnswers

main =
  Browser.sandbox { init = initialModel, update = update, view = view }
Enter fullscreen mode Exit fullscreen mode


quizUI :: ObeliskWidget js t route m => QuizState t -> m (QuizEvents t)
quizUI QuizState{..} = wrapUI do
  selectAnswer <- leftmost <$> for (enumerate allQuestions)
    \(qNum, (questionText, answers)) -> do
    divClass "question" do
      text questionText
    answersUI qNum areAnswersShown answers
  showAnswers <- footerUI canCheckAnswers score
  return QuizEvents{..}
Enter fullscreen mode Exit fullscreen mode

Function leftmost allows combining several events of the same type into one.

leftmost :: Reflex t => [Event t a] -> Event t a
Enter fullscreen mode Exit fullscreen mode

It’s important to note that the events in Reflex can occur simultaneously. This is why it has to be kept in mind that it’s possible to lose something important, as in this case the leftmost ignores all events, except for the first one.

Here we use leftmost to turn the list of events returned as the result of the iteration by the question list into one event. In this case, it’s not possible to click on two answer options simultaneously, so this is safe.

We also use leftmost to create the answer list:

answersUI :: ObeliskWidget js t route m
  => Int
  -> Dynamic t AreAnswersShown
  -> [(Answer, Dynamic t IsChosen)]
  -> m (Event t SelectAnswer)
answersUI qNum areAnswersShown answers = elClass "div" "answers" do
  leftmost <$> for (enumerate answers)
    \(aNum, (Answer{answerText,isCorrect}, dynIsChosen)) -> do
    event <- answerUI areAnswersShown answerText isCorrect dynIsChosen
    return (event $> SelectAnswer { questionNumber = qNum, answerNumber = aNum })
Enter fullscreen mode Exit fullscreen mode

In function answersUI we iterate by the answers list, each time calling function answerUI, where we build a new Dynamic containing the Map from the dynamic attributes of the DOM, which is an answer option. We make use of the fact that the Dynamic is a monad to form the class name for an HTML element with an answer option. We use Writer for the sake of “imperativeness”.

We use $> SelectAnswer { questionNumber = qNum, answerNumber = aNum } to replace the empty value () (called “unit”), which is the payload of the “click” event by default, with the payload we need that shows the answer we clicked on.

answerUI :: ObeliskWidget js t route m
  => Dynamic t AreAnswersShown
  -> AnswerText
  -> IsCorrect
  -> Dynamic t IsChosen
  -> m (Event t ())
answerUI areAnswersShown answerText isCorrect dynIsChosen =
  domEvent Click . fst <$> elDynAttr' "div" dynAttrs do
    text answerText
    dynAttrs = do
    isChosen <- dynIsChosen
    areShown <- areAnswersShown
      className = T.intercalate " " $ execWriter do
        tell ["answer"]
        when (isChosen == Chosen) $ tell ["answer-chosen"]
        tell [ if areShown == AnswersShown
                then "answer-shown"
                else "answer-hidden" ]
        tell [ if isCorrect == Correct
                then "answer-correct"
                else "answer-incorrect" ]
    return $ "class" =: className
Enter fullscreen mode Exit fullscreen mode

Construction domEvent Click . fst <$> elDynAttr'… allows obtaining the click event in the form of a value.

footerUI is a widget containing either a text offering to answer all questions, or a check button, or the information on the results:

footerUI :: ObeliskWidget js t route m
  => Dynamic t CanCheckAnswers -> Dynamic t Score -> m (Event t CheckAnswers)
footerUI canCheckAnswersDyn dynScore = wrapContainer do
  evt <- switchHold never <=< dyn $ do
    canCheckAnswers <- canCheckAnswersDyn
    score <- dynScore
    return if score /= NoScore
    then return never
    else case canCheckAnswers of
      CanCheckAnswers  -> checkAnswersButton
      CantCheckAnswers -> cantCheckNote
  dyn_ $ dynScore <&> \case
    NoScore -> blank
    Score{totalQuestions, correctAnswers} -> do
    text "Your score: "
    text . T.pack $ show correctAnswers
    text " of "
    text . T.pack $ show totalQuestions
  return (evt $> CheckAnswers)
    wrapContainer = divClass "check-answers-button-container"
    checkAnswersButton = do
    domEvent Click . fst <$> do
      el' "button" do
      text "Check answers"
    cantCheckNote = do
    divClass "unfinished-quiz-notice" do
      text "Select answers for all questions before you can get the results."
    return never
Enter fullscreen mode Exit fullscreen mode

Expression switchHold never <=< dyn $ do … can also be often seen in the code. We use it when we want to get an event from a dynamically changing widget.

It makes sense to explain in detail what is going on here.

First of all, the correct way for placing the brackets is as follows: (switchHold never <=< dyn) $ do…

Let’s follow the types:

switchHold :: ... => Event t a -> Event t (Event t a) -> m (Event t a)
never      :: ... => Event t a
(<=<)      :: Monad m => (b -> m c) -> (a -> m b) -> a -> m c
dyn        :: ...  => Dynamic t (m a) -> m (Event t a)
Enter fullscreen mode Exit fullscreen mode
  • switchHold makes it possible to “switch” the events we are subscribed to as they become available. They are received as the payloads of another event. The event passed as the first argument is the one that will be active until the second event occurs.
  • never is just an event that never occurs.
  • (<=<)is the fish we already know.
  • dyn allows “using” a dynamic widget. The returned event activates each time the dynamic changes.

This implies that:

switchHold never <=< dyn :: Dynamic t (m (Event t a)) -> m (Event t a)
Enter fullscreen mode Exit fullscreen mode

It’s clear that at any point in time there will be a widget inside the Dynamic returning the event. This very widget will be “used”.

We use if score /= NoScore then return never… because if the result has been counted already, the event “count the result” must never occur.

A significant difference of the code shown above from the Elm code is that we return the events explicitly and don’t just assign them as the element attributes. The requirement to push the events through is the cost of the ability to process and combine them in any way anywhere we need. However, we don’t always do so in our production code. Sometimes, we can use the type class EventWriter that offers the tellEvent function. tellEvent looks very much like the tell in a standard Writer:

tellEvent :: EventWriter t w m => Event t w -> m ()
tell :: MonadWriter w m => w -> m ()
Enter fullscreen mode Exit fullscreen mode

After fully describing the widget, we can make it operable using the function mkWidget created previously:

mkQuizWidget :: ObeliskWidget js t route m => [(QuestionText, [Answer])] -> m ()
mkQuizWidget qs =
  mkWidget (mkQuizModel qs) quizUI
Enter fullscreen mode Exit fullscreen mode

Widget preview:


Elm’s advantages as compared with Reflex:

  • The tooling is more convenient. Knowledge of Nix is not required; the Elm’s executable file provides everything you need to build the application.
  • The language is simpler for a beginner. Elm is easy to learn as the first functional language.
  • The compilation time, as well as the generated code are much shorter.
  • You don’t need to divide the application status explicitly into the dynamic and static ones.


  • Elm is a less expressive language. In comparison with Haskell, there is an increased need to duplicate the code due to the unavailability of such mechanisms as the type classes (for ad hoc polymorphism), template Haskell (for code generation), and generics (Datatype-generic programming, not to be confused with generics in OOP languages).
  • The Model has to be rebuilt after each message. Even though persistent data structures in purely functional languages can be updated partially (sharing), the very fact that the framework doesn’t “see”, which portion of the data structure has been changed, makes it necessary to use the Virtual DOM.
  • Elm doesn’t support the foreign function interface to call arbitrary JavaScript functions.

Though Elm has several important advantages, we still opted for Reflex as it is rather convenient to use the same language both for the backend and frontend development.

Top comments (2)

kirkcodes profile image
Kirk Shillingford

I really appreciate this write up. I'm a fan of Elm on the front-end, but it was nice to see the approaches laid out side and by side and I can certainly appreciate what both languages bring to the table a bit better now.

Thank you for your efforts putting this all together.

fiercekatie profile image
Catherine Galkina

Thank you for your comment! We're glad you appreciate our work.

50 CLI Tools You Can't Live Without

>> Check out this classic DEV post <<