How do you iterate a List (Maybe a) - elm

I have the following graphQL result:
[Just { details = Just "Engine failure at 33 seconds and loss of
vehicle", launch_year = Just "2006", links = Just { article_link =
Just
"https://www.space.com/2196-spacex-inaugural-falcon-1-rocket-lost-launch.html"
}, mission_name = Just "FalconSat" }]
Based on the following types:
type alias Launch =
{ mission_name : Maybe String
, details : Maybe String
, launch_year : Maybe String
, links : Maybe LaunchLinks
}
type alias Launches =
Maybe (List (Maybe Launch))
type alias LaunchLinks =
{ article_link : Maybe String
}
I want to List.map through and display the results in unordered list. I started with this:
renderLaunch : Launches -> Html Msg
renderLaunch launches =
div [] <|
case launches of
Nothing ->
[ text "Nothing here" ]
Just launch ->
launch
|> List.map (\x -> x)
|> ul []
But I keep getting this error:
This function cannot handle the argument sent through the (|>) pipe:
141| launch 142| |> List.map (\x
-> x) 143| |> ul []
^^^^^ The argument is:
List (Maybe Launch)
But (|>) is piping it a function that expects:
List (Html msg)

The problem is that the Just launch case needs to result in a List (Html msg) but the code results in a different type being returned.
When you are using List.map (\x -> x), it is essentially a no-op. You are iterating over a List (Maybe Launch) and returning the same thing. I'd recommend creating another function that takes a Maybe Launch value and use that as your mapping function. For example:
displayLaunch : Maybe Launch -> Html Msg
displayLaunch launch =
case launch of
Nothing -> text "No launch"
Just l -> text (Debug.toString l)
Now you can plug that into your mapping function:
Just launch ->
launch
|> List.map displayLaunch
|> ul []
But, whoops! Now you get a new error indicating:
The 2nd branch is:
Html Msg
But all the previous branches result in:
List (Html msg)
The problem here is that we are now returning a ul from the Just launch branch and we need to return a list of html. You can use List.singleton to create a list with just one item:
Just launch ->
launch
|> List.map displayLaunch
|> ul []
|> List.singleton

Related

Elixir - Manipulating a 2 dimensional list

Hope everybody is having a beautiful 2019 even though we're just a day in.
I am currently working on a small Phoenix app where I'm manipulating PDF files (in the context of this question I'm splitting them) and then uploading them to S3. Later on I have to delete the temporary files created by pdftk ( a pdf tool ) I use to split them up and also show the s3 links in the response body since this is an API request.
The way I have structured this is as following:
Inside my Split module where the core business logic is:
filenames = []
s3_links = []
Enum.map(pages, fn(item) ->
split_filename = item
|> split(filename)
link = split_filename
|> FileHelper.result_file_bytes()
|> ManageS3.upload()
|> FileHelper.save_file(work_group_id, pass)
[filenames ++ split_filename, s3_links ++ link]
end)
|> transform()
{filenames, s3_links}
The important things are split_filename and link
This is what I'm getting when I call an IO.inspect in the transform() method:
[
["87cdcd73-5b27-4757-a472-78aaf6cc6864.pdf",
"Some_S3_LINK00"],
["0ab460ca-5019-4864-b0ff-343966c7d72a.pdf",
"Some_S3_LINK01"]
]
The structuring is [[filename, s3_link], [filename, s3_link]] whereas the desired outcome would be that of [ [list of all filenames], [list of s3 links].
If anybody can lend a hand I would be super grateful. Thanks in advance!
Sidenotes:
Assigning filenames = []; s3_links = [] in the very beginning makes zero sense. Enum.map already maps the input. What you need is probably Enum.reduce/3.
Don’t use the pipe |> operator when the pipe consists of the only call, it is considered an anti-pattern by Elixir core team.
Always start pipes with a term.
Solution:
Reduce the input into the result using Enum.reduce/3 directly to what you need.
pages
|> Enum.reduce([[], []], fn item, [files, links] ->
split_filename = split(item, filename)
link =
split_filename
|> FileHelper.result_file_bytes()
|> ManageS3.upload()
|> FileHelper.save_file(work_group_id, pass)
[[split_filename | files], [link | links]]
end)
|> Enum.map(&Enum.reverse/1)
|> IO.inspect(label: "Before transform")
|> transform()
You did not provide the input to test it, but I believe it should work.
Instead of working on lists of lists, you may want to consider using tuples with lists. Something like the following should work for you.
List.foldl(pages, {[], []}, fn(item, {filenames, links}) ->
filename = split(item, filename)
link =
file_name
|> FileHelper.result_file_bytes()
|> ManagerS3.upload()
|> FileHelper.save_file(work_group_id, pass)
{[filename | filenames], [link | links]}
end)
This will return a value that looks like
{
["87cdcd73-5b27-4757-a472-78aaf6cc6864.pdf",
"0ab460ca-5019-4864-b0ff-343966c7d72a.pdf"],
["Some_S3_LINK00",
"Some_S3_LINK01"]
}
Though, depending on how you are using these values, maybe a list of tuples would be more appropriate. Something like
Enum.map(pages, fn(item) ->
filename = split(item, filename)
link =
filename
|> FileHelper.result_file_bytes()
|> ManageS3.upload()
|> FileHelper.save_file(work_group_id, pass)
{filename, link}
end)
would return
[
{"87cdcd73-5b27-4757-a472-78aaf6cc6864.pdf", "Some_S3_LINK00"},
{"0ab460ca-5019-4864-b0ff-343966c7d72a.pdf", "Some_S3_LINK01"}
]

Retrieving a DOM value from Elm ports

My elm app uses an auto scrolling function, which gets the Y position of an element and uses Dom.Scroll.toY to scroll there.
Two do this, I set up two ports; a subscription and sender.
ports.elm
port setYofElementById : Maybe String -> Cmd msg
port getYofElementById : (Value -> msg) -> Sub msg
index.html
app.ports.setYofElementById.subscribe(function(id) {
var element = document.getElementById(id);
var rect = element.getBoundingClientRect();
app.ports.getYofElementById.send({"number": rect.top});
})
The listener is a subscription
subscriptions : Model -> Sub Msg
subscriptions model =
Ports.getYofElementById getYofElementById
getYofElementById : Decode.Value -> Msg
getYofElementById value =
let
result =
Decode.decodeValue bSimpleIntValueDecoder value
in
case result of
Ok simpleIntValue ->
SetSelectedElementYPosition (Just simpleIntValue.number)
Err id ->
SetSelectedElementYPosition Nothing
SetSelectedElementYPosition just sets the model.
Now, the action that executes this does two things: call Port.setYofElementById, then scrolls to the Y value in the model, assuming that it has already been set.
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
ScrollToY idString ->
model
=> Cmd.batch
[ Ports.setYofElementById (Just idString)
, Task.attempt (always NoOp) <| Dom.Scroll.toY "ul" model.selectedElementYPosition
]
However, this doesn't happen sequentially. When the action first fires, nothing happens. If I fire it again, it scrolls to the location called for in the first action. So it seems like it is calling Dom.Scroll.toY before the value is set.
Is there a way to force the Cmds in ScrollToY to happen in sequence? Or is there a better way to do this in general?
You can get the Cmds to execute in sequence by making the second, the one that does the Dom.Scroll.toY, happen as a response to the first, the one that does the setYofElementById. The following update function accomplishes this:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
ScrollToY idString ->
(model, Ports.setYofElementById idString)
SetSelectedElementYPosition (Just newY) ->
(model, Task.attempt (always NoOp) <| Dom.Scroll.toY "ul" newY)
SetSelectedElementYPosition Nothing ->
(model, Cmd.none)
NoOp ->
(model, Cmd.none)
With the Cmds correctly sequenced, you will need to make sure that the newY argument to Dom.Scroll.toY is in the correct frame of reference to get the effect that you want.
I finally got this to work by tacking the action for Task.attempt (always NoOp) <| Dom.Scroll.toY "ul" model.selectedElementYPosition onto the action called by the subscription, not the action. That's the key.
With ports, the subscribe and send actions follow completely different pathways, so anything that reacts to a send from js to elm is not going to be referenced in the actions that go from elm to js.
In this case, since SetSelectedElementYPosition is being called from the subscription, you have to set the update there:
SetSelectedElementYPosition idString ->
({model | selectedElementYPosition = number }, Cmd.none)
|> andThen update GoToSelectedElementYPosition

Basic Chat using elm-native-ui

I'm creating a basic native chat app with elm-native-ui.
I can open or close the chat by clicking on a button that change the chatOpen boolean
activeChannelView =
case chatOpen of
True ->
Maybe.map (\a -> chatView users a messages) activeChannel ? viewEmpty
False ->
Maybe.map (cardView users) activeChannel ? viewEmpty
on the False side, it’s working well but on the True side it says that viewEmpty needs to be a (String -> Node Msg) instead of being (Node Msg) but the left side (chatView user a messages) returns Node Msg
Check the error below
The right side of (?) is causing a type mismatch.
Maybe.map (\a -> chatView users a messages) activeChannel ? viewEmpty
^^^^^^^^^
(?) is expecting the right side to be a:
String -> Node Msg
But the right side is:
Node Msg
I suppose it means that:
if activeChannel is (Just a), True returns (String -> Node Msg)
How is it possible to have this error since chatView is defined like that:
chatView : List User -> Request -> List String -> String -> Node Msg
chatView takes four parameters and you are passing only three.

How to filter a Signal on page load

For the sake of learning, I'm trying to load content only when I click on a button. So far I've managed to :
Reload the content when I click the button.
And Filter the Signal when I click (if the String I send is not "GETPERF")
But my problem is that the Ajax call is still triggered once the page loads.
Here's the code:
-- SIGNALS & MAILBOX
inbox : Signal.Mailbox String
inbox =
Signal.mailbox "SOME TEXT"
result : Signal.Mailbox String
result =
Signal.mailbox ""
-- VIEW
view : String -> Html
view msg =
div [] [
h1 [] [text "Mailbox3"],
p [] [text msg],
button
[onClick inbox.address "GETPERF"]
[text "click perf"],
]
main : Signal Html
main =
Signal.map view result.signal
-- TASK & EFFECTS
port fetchReadme : Signal (Task Http.Error ())
port fetchReadme =
inbox.signal
|> Signal.filter (\sig -> sig == "GETPERF" ) "boo"
|> Signal.map (\_ -> Http.getString "http://localhost:3000/dates" `andThen` report)
report : String -> Task x ()
report html =
Signal.send result.address html
Is there any way to prevent the first Ajax call on page load ? (Or some more idiomatic way of doing all this ?)
The reason you're getting an initial ajax request is that Signal.filter is still keeping that initial value of "boo" (See the Signal.filter documentation here). That value is ignored in the next Signal.map statement by your use of the underscore parameter, but the Http Task is still getting returned and that's why you see an initial ajax request on page load.
Instead of using Signal.filter, you could write a conditional that only sends the ajax request in the correct circumstances, when sig is "GETPERF". And if sig is not "GETPERF" (as in page load), you can, in essence, do nothing by returning Task.succeed (). Here is a refactored fetchReadme function with these changes:
port fetchReadme : Signal (Task Http.Error ())
port fetchReadme =
let
fetchAndReport sig =
if sig == "GETPERF" then
Http.getString "http://localhost:3000/dates"
`andThen` report
else
Task.succeed ()
in
Signal.map fetchAndReport inbox.signal

How to get query parameters in Elm?

In my Elm program, I'd like to initialize my model based on the query string.
For example, if the query string is ?w=3&h=5 I'd like to have:
initialModel =
{ width = 3
, height = 5
}
Is that possible to achieve this in Elm, or the only way to do this is to get the query parameters in Javascript and pass them via a port?
Elm 0.19
For elm 0.19 the below concept is the same. Both of these packages still exist but have been moved and relabeled as the official elm/url and elm/browser libraries.
Elm 0.18
This example uses evancz/url-parser and elm-lang/navigation. There are a few kinks that aren't straightforward in the documentation, but I've explained them briefly below. The example should speak for itself.
module Main exposing (..)
import Html as H exposing (..)
import Navigation exposing (Location)
import UrlParser as UP exposing ((</>), (<?>), top, parsePath, oneOf, s, stringParam, Parser)
import Maybe.Extra as MaybeExtra exposing (unwrap)
type Route
= UrlRoute (Maybe String) (Maybe String)
| NotFoundRoute
type Msg
= UrlParser Navigation.Location
type alias Model =
{ location : Route
, w : String
, h : String
}
type alias SearchParams =
{ w : Maybe String, h : Maybe String }
main =
Navigation.program UrlParser
{ init = init
, view = view
, update = update
, subscriptions = (\_ -> Sub.none)
}
init : Location -> ( Model, Cmd Msg )
init location =
let
currentPath =
parseLocation location
in
( initialModel currentPath
, Cmd.none
)
parseLocation : Location -> Route
parseLocation location =
case (parsePath matchers location) of
Just route ->
route
Nothing ->
NotFoundRoute
matchers : Parser (Route -> a) a
matchers =
UP.map UrlRoute (UP.s "index" <?> UP.stringParam "w" <?> UP.stringParam "h")
initialModel : Route -> Model
initialModel route =
{ location = route
, w = MaybeExtra.unwrap "" (\x -> Maybe.withDefault "" x.w) (parseParams route)
, h = MaybeExtra.unwrap "" (\x -> Maybe.withDefault "" x.h) (parseParams route)
}
parseParams : Route -> Maybe SearchParams
parseParams route =
case route of
UrlRoute w h ->
Just { w = w, h = h }
NotFoundRoute ->
Nothing
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
UrlParser location ->
( model
, Cmd.none
)
view : Model -> Html msg
view model =
div []
[ h1 [] [ text "URL Info" ]
, div [] [ text ("W is: " ++ model.w) ]
, div [] [ text ("H is: " ++ model.h) ]
]
The "trick" is to create another type alias to place your query params inside of. In the above example I've created the type SearchParams. After creating this type we just use an initialModel that takes in the currentPath.
From there, our model can extract the query params with Maybe.withDefault (it needs to be a Maybe type because the params may not be there). Once we have our data in the model we just print it out in the view.
Hope this helps!
There is no built-in core library way to access the URL. You can use ports and the community library jessitron/elm-param-parsing.
If you also want to set the URL, you can again use ports, or you can use the History API, for which there are bindings in TheSeamau5/elm-history.
Unfortunately jessitron/elm-param-parsing doesn't work with Elm 0.18.
Use elm-lang/navigation package:
http://package.elm-lang.org/packages/elm-lang/navigation/latest/Navigation
https://github.com/elm-lang/navigation/tree/2.1.0
especially this function:
program
: (Location -> msg)
-> { init : Location -> (model, Cmd msg), update : msg -> model -> (model, Cmd msg), view : model -> Html msg, subscriptions : model -> Sub msg }
-> Program Never model msg
In the second parameter you can see "init : Location -> (model, Cmd msg)". This should handle reading of initial URL. To complement that, first parameter is a function which gets called every time URL changes.
(I am aware it's an old question, but this link popped out when I was looking for the solution to the same problem and accepted answer didn't help)