Elm: How to pretty print the model in the browser? - elm

This question is kind of silly but i didn't find a straight forward solution.
Assuming I have a model that resembles this: - at least this big.
initModel =
{ selectedCategory = "Vacantion"
, context = "root/Work"
, abstractSyntaxTree =
[ { categoryName = "Work"
, categoryContent =
[]
}
, { categoryName = "Vacation"
, categoryContent =
[ { folderName = "Hawaii"
, folderContent =
FolderContent
( [ { folderName = "Booking"
, folderContent = FolderContent ( [], [] )
}
]
, [ "flightTicket.jpg" ]
)
}
]
}
]
}
Question: How can i display it in the browser so that it looks good? - nothing fancy - just to see what's happening like a quick debugger..
What I've tried so far:
view =
div []
[ pre [style [("width", "300") ] ] [ text (toString model)]
]
Works great on smaller models but on this case, I get this long -single line - formatted json like structure:
I think this is the Problem: The prettify extension i installed in google chrome doesn't know how to deal with strings that do not contain \n in them. To check for this, i manually added a \n - and that string was split on the second row, as expected.
The output form text (toSting model) - it's a string that has no \n in it - so that's why everything is displayed on a single line in the browser - regardless of the 300 px limit on width.
Splitting up the string - by adding \n by myself - works except i don't know where exactly to add the \n. To make it dynamic, this requires a full-flagged parser for the model. A way to know where the expression starts, where is the next matching bracket .. etc. I'm not good enough to build this parser. I feel i'm over complicating this stuff. Must be a better solution..
How do you folks do it?

Elm does not allow you to enumerate over the items in a record. For type-safety no doubt. So there is no "clean" way to do display a record nicely.
UPDATE: The solution below will not work anymore as of Elm 0.19.
It relies on a (deprecated) function toString that would convert any type into a string.
Since Elm 0.17 (when I made this), Elm had already released a great debugger in 0.18, which already has a feature to follow model and messages in a separate window.
For debugging, you can do some trickery with toString to print it more neatly.
Below is example code, which you can copy/ paste to elm-lang.org/try.
Simply pass any record to the viewModel function, and it displays it in the browser.
It is quite crude, and probably not very performant with large records, but it will do the job..
import Html exposing (Html, text, div, p, pre)
import Html.Attributes exposing (style)
import String
quote = "\""
indentChars = "[{("
outdentChars = "}])"
newLineChars = ","
uniqueHead = "##FORMAT##"
incr = 20
model =
{ name = "Abe"
, age = 49
, someTuple = (18,49)
, relatives = [ "Claire", "Bill" ]
, comments = "any special characters like []{}, will not be parsed"
, cars = [ { brand = "BMW", model = "535i" } ]
}
viewModel : a -> Html msg
viewModel model =
let
lines =
model
|> toString
|> formatString False 0
|> String.split uniqueHead
in
pre [] <| List.map viewLine lines
viewLine : String -> Html msg
viewLine lineStr =
let
(indent, lineTxt) = splitLine lineStr
in
p [ style
[ ("paddingLeft", px (indent))
, ("marginTop", "0px")
, ("marginBottom", "0px")
]
]
[ text lineTxt ]
px : Int -> String
px int =
toString int
++ "px"
formatString : Bool -> Int -> String -> String
formatString isInQuotes indent str =
case String.left 1 str of
"" -> ""
firstChar ->
if isInQuotes then
if firstChar == quote then
firstChar
++ formatString (not isInQuotes) indent (String.dropLeft 1 str)
else
firstChar
++ formatString isInQuotes indent (String.dropLeft 1 str)
else
if String.contains firstChar newLineChars then
uniqueHead ++ pad indent ++ firstChar
++ formatString isInQuotes indent (String.dropLeft 1 str)
else if String.contains firstChar indentChars then
uniqueHead ++ pad (indent + incr) ++ firstChar
++ formatString isInQuotes (indent + incr) (String.dropLeft 1 str)
else if String.contains firstChar outdentChars then
firstChar ++ uniqueHead ++ pad (indent - incr)
++ formatString isInQuotes (indent - incr) (String.dropLeft 1 str)
else if firstChar == quote then
firstChar
++ formatString (not isInQuotes) indent (String.dropLeft 1 str)
else
firstChar
++ formatString isInQuotes indent (String.dropLeft 1 str)
pad : Int -> String
pad indent =
String.padLeft 5 '0' <| toString indent
splitLine : String -> (Int, String)
splitLine line =
let
indent =
String.left 5 line
|> String.toInt
|> Result.withDefault 0
newLine =
String.dropLeft 5 line
in
(indent, newLine)
main =
viewModel model

By now we have a nice package for 0.19.
elm install ThinkAlexandria/elm-pretty-print-json
And run
json = """{"name": "Arnold", "age": 70, "isStrong": true,"knownWeakness": null,"nicknames": ["Terminator", "The Governator"],"extra": {"foo": "bar","zap": {"cat": 1,"dog": 2},"transport": [[ "ford", "chevy" ],[ "TGV", "bullet train", "steam" ]]}}"""
{-| Formating configuration.
`indent` is the number of spaces in an indent.
`columns` is the desired column width of the formatted string. The formatter
will try to fit it as best as possible to the column width, but can still
exceed this limit. The maximum column width of the formatted string is
unbounded.
-}
config = {indent = 4, columns = 80}
-- run prettifier
Result.withDefault "" (Json.Print.prettyString config json)
-- result
{-
{
"extra": {
"transport": [
[
"ford",
"chevy"
],
[
"TGV",
"bullet train",
"steam"
]
],
"zap": {
"dog": 2,
"cat": 1
},
"foo": "bar"
},
"nicknames": [
"Terminator",
"The Governator"
],
"knownWeakness": null,
"isStrong": true,
"age": 70,
"name": "Arnold"
}
-}

I found this in slack : elm-debug-transformer
Not rely what i asked for, because for example im gonna need to make it work with node.js also, but still seems like good solution for now.

Related

What is the meaning of alias "#" in GF?

I have encountered alias sign inside RGL in this operation:
mkRoot3 : Str -> Root3 = \fcl -> case fcl of {
f#? + c#? + l => {f = f ; c = c ; l = l} ;
_ => error ("mkRoot3: too short root" ++ fcl)
} ;
What does it mean, and what's the use of it?
The character # is used in pattern matching. The expression foo#bar means that you match something with the regex bar, and bind the result to variable foo.
Let's first recap some ways you can pattern match a string without #:
example1 : Str -> Str = \s -> case s of {
"x" => "the string is exactly x" ;
"x" + _ => "the string starts with x" ;
? => "the string is one character long" ;
? + ? => "the string is two characters long" ;
("a"|"i"|"u") => "the string is a vowel" ;
_ => "whatever, I'm bored"
} ;
In all of these, we are not reusing the string in the right-hand side. If you want to do that, you can just bind it into a variable, like this—not yet using # , because we are simply matching the start and the end of a string:
example2 : Str -> Str = \s -> case s of {
"x" + rest => "the string starts with x and ends with" ++ rest ;
start + "x" => "the string starts with" ++ start ++ "and ends with x" ;
_ => "..." } ;
Finally, if you want to match something to a regular expression and use whatever matches the regex on the RHS, now you need to use #:
example3 : Str -> Str = \s -> case s of {
v#("a"|"i"|"u") => "the string is the vowel" ++ v ;
a#? => "the string is one character long:" ++ a ;
a#? + b#? => "the string is two characters long:" ++ a ++ "followed by" ++ b ;
_ => "..." } ;
If you just tried to match a + b => … , or any other pattern that only contains variables, it wouldn't match words that are exactly 2 characters long. Instead it would just match empty string to one of them and the full string to the other.
So matching the regex ?, which matches exactly one character, and then binding the result to a variable, is the only way you can match something with exact length, and then reuse the matched character/string on the right-hand side.
You can read more on pattern matching at http://www.grammaticalframework.org/doc/gf-refman.html#pattern-matching .

Decode JSON where value can be a string or an array of different values

Yet another question on how to decode things with Elm...
So the problem is that I need to decode a value which can be either a string, for example
"price_unknown"
or it can be an array of 2 elements, where first one is a string and the second is a float:
["price", 50.5]
And for the eventual value I have a type:
type Something
= PriceUnknown
= Price Float
into which I need to convert the value from the json response.
I tried to use a bunch of things using somewhat between the lines of:
decode MyString
|> required "other_value" Json.Decode.string
|> required "price" JD.oneOf [JD.string, JD.list (JD.oneOf [JD.string, JD.float])] |> JD.map mapper
( I use json_decode_pipeline package here )
But obviously it complains about different values in the lists and whatnot so I am stuck.
Thank you in advance.
You are pretty close, but all of the Decoders in oneOf have to have the same type. Also, destructuring mixed lists can be a pain. This uses elm-community/json-extra to make a manual decoding step easier.
myDecoder : Decoder SomethingElse
myDecoder =
decode MyString
|> required "other_value" Json.Decode.string
|> required "price" priceDecoder
priceDecoder : Decoder Something
priceDecoder =
JD.oneOf
[ priceUnknownDecoder
, priceKnownDecoder
]
priceUnknownDecoder : Decoder Something
priceUnknownDecoder =
JD.string
|> JD.andThen
(\string ->
if string == "price_unknown" then
JD.succeed PriceUnknown
else
JD.fail "Invalid unknown price string."
)
priceKnownDecoder : Decoder Something
priceKnownDecoder =
listTupleDecoder
JD.string
JD.float
|> JD.andThen
(\(tag, price) ->
if tag == "price" then
JD.succeed (Price price)
else
JD.fail "Invalid tag string."
)
listTupleDecoder : Decoder a -> Decoder b -> Decoder (a, b)
listTupleDecoder firstD secondD =
JD.list JD.value
|> JD.andThen
(\values ->
case values of
[first, second] ->
Result.map2
(,)
JD.decodeValue firstD first
JD.decodeValue secondD second
|> JD.Extra.fromResult
_ ->
JD.fail "There aren't two values in the list."
)

Elm Multiple Counter code using List

Im new to Elm , previously I was doing React, Redux , ES6
Need help optimizing the below counter example using Array rather than List.
Getting error Type mismatch When I tried to use Array Set/Get methods.
import Html exposing (beginnerProgram, div, button, text, p, Html, span)
import Html.Events exposing (onClick)
import Array
main =
beginnerProgram { model = [0,7], view = view, update = update }
view model =
div []
[ div [] [ button [ onClick AddCounter ] [ text "Add Counter" ] ]
, div [] (List.indexedMap (\index data -> (viewCounter index data)) model)
]
viewCounter: Int -> Int -> Html Msg
viewCounter index data =
div [] [
p [] [text ("Counter " ++ toString index )]
, button [ onClick (Decrement index) ] [ text "-" ]
, span [] [ text (toString data) ]
, button [ onClick (Increment index) ] [ text "+" ]
]
type Msg =
AddCounter
| Increment Int
| Decrement Int
incrementSelectedIndex index selectedIndex data =
if index == selectedIndex then data + 1
else data
decrementSelectedIndex index selectedIndex data =
if index == selectedIndex then data + 1
else data
update msg model =
case msg of
AddCounter ->
List.append model [0]
Increment selectedIndex ->
(List.indexedMap (\index data ->
(incrementSelectedIndex index selectedIndex data)) model)
Decrement selectedIndex ->
(List.indexedMap (\index data ->
(decrementSelectedIndex index selectedIndex data)) model)
Array.get returns a Maybe a since the requested index may not be found. This may be different than other languages with which you have experience, which tend to throw exceptions when an index is out of bounds.
In Elm, the Maybe type has two possible values: Nothing, and Just a where a is the actual value you care about.
You can use a case statement to differentiate between the two options. Here is an example of what your Increment case can look like using Array's get and set functions:
Increment selectedIndex ->
case Array.get selectedIndex model of
Just val -> Array.set selectedIndex (val + 1) model
Nothing -> model
Notice that if we ask for a nonexistent index, we would get Nothing and just return the original array rather than crashing the program.

Elm JSON Decoder Array of Objects

I am doing a http request with elm and my response is an Array of Objects. Each object is as follows
obj = {
title: "Some Title",
words: [ "word1", "word2", "word3", "word4" ]
}
Here is my decoder so far:
type alias ThisRes = List ResObj
type alias ResObj =
title: String
words: List String
decoded : Decoder ThisRes
decoded =
decode ThisRes
I can't seem to get the decoder right and any help that can be provided would be appreciated.
obj =
"""
{
"title": "Some Title",
"words": [ "word1", "word2", "word3", "word4" ]
}
"""
type alias ResObj =
{ title : String, words : List String }
objDecoder =
map2 ResObj
(at [ "title" ] string)
(at [ "words" ] (list string))
headingFrom : Result String ResObj -> String
headingFrom result =
case result of
Ok resobj ->
resobj.title
Err reason ->
toString reason
main =
h1 [] [ text <| headingFrom <| decodeString objDecoder obj ]
Breaking this down:
obj is just a string representing some JSON, to illustrate the example.
You define the ResObj type alias, and get a type constructor for it. Now you can create ResObj values by calling ResObj "MyTitle" ["word1", "wordb", "wordFightingMongooses"].
objDecoder is a value that helps decode JSON into ResObj. It calls map2, which takes a type constructor, and then two decoded JSON fields. (at ["title"] string) says "turn the value at title in the JSON into a string" and you can guess what the next line does. So this evaluates to, ultimately, ResObj "SomeTitle" ["word1", "word2", "word3", "word4"] and creates your value.
Down in our main, we have the expression decodeString objDecoder obj. decodeString takes a decoder value, and a JSON string, and sees if it can decode it. But it doesn't return the ResObj itself -- because the decoding can fail, it returns a Result.
So we have to process that Result with our function headingFrom. If the decoding succeeded our result will be Ok resobj, and we can work with the resobj finally. If it failed it'll be Err reason.

Why is elm so picky about whitespace but only in certain cases?

Why does this code fail
type SectionedItems s i = SectionedItems{
section : s,
items : List i,
subsections: List (SectionedItems s i)
}
si1 : SectionedItems String String
si1 = SectionedItems{
section = "",
items = [
"1",
"2"
],
subsections = [
]
}
and this code succeeds
type SectionedItems s i = SectionedItems{
section : s,
items : List i,
subsections: List (SectionedItems s i)
}
si1 : SectionedItems String String
si1 = SectionedItems{
section = "",
items = [
"1",
"2"
],
subsections = [
]
}
Why does elm fail for the first code? I know it is failing due to whitespace but why? Why do the { and } have to be aligned when creating an instance but not when declaring the type?
It's not that those brackets have to be lined up, it's that you can't put the closing bracket at the start of the line.
For example, this compiles:
si1 : SectionedItems String String
si1 = SectionedItems{
section = "",
items = [
"1",
"2"
],
subsections = [
]
}
Just putting in one extra space before the closing bracket is enough.
Why? Because the "children" of si1 must have greater indentation than si1 itself. If they don't, Elm thinks you're trying to start a new definition, and } isn't a valid way to start a definition.