Skip to content

Migrate from ContT to Aff #208

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Dec 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion client/spago.dhall
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ You can edit this file as you like.
-}
{ name = "try-purescript"
, dependencies =
[ "arrays"
[ "aff"
, "affjax"
, "arrays"
, "bifunctors"
, "console"
, "const"
, "contravariant"
, "control"
, "debug"
, "distributive"
, "effect"
, "either"
Expand Down
103 changes: 51 additions & 52 deletions client/src/Main.purs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ module Main where

import Prelude

import Control.Monad.Cont.Trans (ContT(..), runContT)
import Control.Monad.Except.Trans (runExceptT)
import Data.Array (mapMaybe)
import Data.Array as Array
Expand All @@ -11,6 +10,8 @@ import Data.Foldable (elem, fold, for_, intercalate, traverse_)
import Data.FoldableWithIndex (forWithIndex_)
import Data.Maybe (Maybe(..), fromMaybe)
import Effect (Effect)
import Effect.Aff (Aff, launchAff_)
import Effect.Class (liftEffect)
import Effect.Console (error)
import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn5, mkEffectFn1, runEffectFn1, runEffectFn2, runEffectFn5)
import Foreign (renderForeignError)
Expand Down Expand Up @@ -132,31 +133,32 @@ compile = do
displayLoadingMessage
clearAnnotations

runContT (runExceptT (API.compile Config.compileUrl code)) \res_ ->
launchAff_ $ runExceptT (API.compile Config.compileUrl code) >>= \res_ ->
case res_ of
Left err -> displayPlainText err
Left err -> liftEffect $ displayPlainText err
Right res -> do
cleanUpMarkers
liftEffect cleanUpMarkers

case res of
Right (CompileSuccess (SuccessResult { js, warnings })) -> do
showJs <- isShowJsChecked
if showJs
then do hideLoadingMessage
displayPlainText js
else runContT (runExceptT $ runLoader loader (JS js)) \sources -> do
hideLoadingMessage
for_ warnings \warnings_ -> do
let toAnnotation (CompileWarning{ errorCode, position, message }) =
position <#> \(ErrorPosition pos) ->
{ row: pos.startLine - 1
, column: pos.startColumn - 1
, type: "warning"
, text: message
}
runEffectFn1 setAnnotations (mapMaybe toAnnotation warnings_)
for_ sources (execute (JS js))
Right (CompileFailed (FailedResult { error })) -> do
showJs <- liftEffect isShowJsChecked
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes are largely indentation related; the only real difference is the introduction of liftEffect and switching runContT for binds in Aff.

if showJs then liftEffect do
hideLoadingMessage
displayPlainText js
else do
sources <- runExceptT $ runLoader loader (JS js)
liftEffect hideLoadingMessage
for_ warnings \warnings_ -> liftEffect do
let toAnnotation (CompileWarning{ errorCode, position, message }) =
position <#> \(ErrorPosition pos) ->
{ row: pos.startLine - 1
, column: pos.startColumn - 1
, type: "warning"
, text: message
}
runEffectFn1 setAnnotations (mapMaybe toAnnotation warnings_)
for_ sources (liftEffect <<< execute (JS js))
Right (CompileFailed (FailedResult { error })) -> liftEffect do
hideLoadingMessage
case error of
CompilerErrors errs -> do
Expand All @@ -180,7 +182,7 @@ compile = do
pos.endLine
pos.endColumn
OtherError err -> displayPlainText err
Left errs -> do
Left errs -> liftEffect do
hideLoadingMessage
displayPlainText "Unable to parse the response from the server"
traverse_ (error <<< renderForeignError) errs
Expand All @@ -196,7 +198,6 @@ execute js modules = do
setupEditor :: forall r. { code :: String | r } -> Effect Unit
setupEditor { code } = do
loadOptions

setTextAreaContent code
runEffectFn1 setEditorContent code

Expand All @@ -214,34 +215,32 @@ setupEditor { code } = do
compile

JQuery.select "#gist_save" >>= JQuery.on "click" \e _ ->
publishNewGist
launchAff_ publishNewGist

compile
cacheCurrentCode

loadFromGist
:: String
-> ({ code :: String } -> Effect Unit)
-> Effect Unit
loadFromGist id_ k = do
runContT (runExceptT (getGistById id_ >>= \gi -> tryLoadFileFromGist gi "Main.purs")) $
case _ of
Left err -> do
window >>= alert err
k { code: "" }
Right code -> k { code }
-> Aff { code :: String }
loadFromGist id = do
runExceptT (getGistById id >>= \gi -> tryLoadFileFromGist gi "Main.purs") >>= case _ of
Left err -> do
liftEffect $ window >>= alert err
pure { code: "" }
Right code ->
pure { code }

withSession
:: String
-> ({ code :: String } -> Effect Unit)
-> Effect Unit
withSession sessionId k = do
state <- tryRetrieveSession sessionId
-> Aff { code :: String }
withSession sessionId = do
state <- liftEffect $ tryRetrieveSession sessionId
case state of
Just state' -> k state'
Just state' -> pure state'
Nothing -> do
gist <- fromMaybe Config.mainGist <$> getQueryStringMaybe "gist"
loadFromGist gist k
gist <- liftEffect $ fromMaybe Config.mainGist <$> getQueryStringMaybe "gist"
loadFromGist gist

-- | Cache the current code in the session state
cacheCurrentCode :: Effect Unit
Expand All @@ -254,22 +253,21 @@ cacheCurrentCode = do
Nothing -> error "No session ID"

-- | Create a new Gist using the current content
publishNewGist :: Effect Unit
publishNewGist :: Aff Unit
publishNewGist = do
ok <- window >>= confirm (intercalate "\n"
ok <- liftEffect $ window >>= confirm (intercalate "\n"
[ "Do you really want to publish this code as an anonymous Gist?"
, ""
, "Note: this code will be available to anyone with a link to the Gist."
])
when ok do
content <- getTextAreaContent
runContT (runExceptT (uploadGist content)) $
case _ of
Left err -> do
window >>= alert "Failed to create gist"
error ("Failed to create gist: " <> err)
Right gistId -> do
setQueryStrings (Object.singleton "gist" gistId)
content <- liftEffect $ getTextAreaContent
runExceptT (uploadGist content) >>= case _ of
Left err -> liftEffect do
window >>= alert "Failed to create gist"
error ("Failed to create gist: " <> err)
Right gistId -> liftEffect do
setQueryStrings (Object.singleton "gist" gistId)

-- | Navigate to the specified URL.
navigateTo :: String -> Effect Unit
Expand Down Expand Up @@ -308,5 +306,6 @@ main = JQuery.ready do
viewMode <- JQueryExtras.filter jq ":checked" >>= JQueryExtras.getValueMaybe
changeViewMode viewMode

runContT (do sessionId <- ContT createSessionIdIfNecessary
ContT (withSession sessionId)) setupEditor
createSessionIdIfNecessary \sessionId -> launchAff_ do
code <- withSession sessionId
liftEffect $ setupEditor code
23 changes: 0 additions & 23 deletions client/src/Try/API.js

This file was deleted.

64 changes: 29 additions & 35 deletions client/src/Try/API.purs
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,19 @@ module Try.API

import Prelude

import Affjax (URL, printError)
import Affjax as AX
import Affjax.RequestBody as AXRB
import Affjax.ResponseFormat as AXRF
import Affjax.StatusCode (StatusCode(..))
import Control.Alt ((<|>))
import Control.Monad.Cont.Trans (ContT(ContT))
import Control.Monad.Except (runExcept)
import Control.Monad.Except.Trans (ExceptT(ExceptT))
import Control.Monad.Except (ExceptT(..), runExcept)
import Data.Either (Either(..))
import Data.Generic.Rep (class Generic)
import Data.List.NonEmpty (NonEmptyList)
import Data.Maybe (Maybe)
import Effect (Effect)
import Effect.Uncurried (EffectFn1, EffectFn3, EffectFn4, mkEffectFn1, runEffectFn3, runEffectFn4)
import Foreign (Foreign, ForeignError)
import Data.Maybe (Maybe(..))
import Effect.Aff (Aff)
import Foreign (ForeignError, unsafeToForeign)
import Foreign.Class (class Decode, decode)
import Foreign.Generic (defaultOptions, genericDecode)
import Foreign.Generic.Class (Options, SumEncoding(..))
Expand Down Expand Up @@ -123,31 +125,23 @@ instance decodeCompileResult :: Decode CompileResult where
CompileSuccess <$> genericDecode decodingOptions f
<|> CompileFailed <$> genericDecode decodingOptions f

foreign import get_
:: EffectFn3
String
(EffectFn1 String Unit)
(EffectFn1 String Unit)
Unit

-- | A wrapper for `get` which uses `ContT`.
get :: String -> ExceptT String (ContT Unit Effect) String
get uri = ExceptT (ContT \k -> runEffectFn3 get_ uri (mkEffectFn1 (k <<< Right)) (mkEffectFn1 (k <<< Left)))

-- | POST the specified code to the Try PureScript API, and wait for
-- | a response.
foreign import compile_
:: EffectFn4
String
String
(EffectFn1 Foreign Unit)
(EffectFn1 String Unit)
Unit

-- | A wrapper for `compileApi` which uses `ContT`.
compile
:: String
-> String
-> ExceptT String (ContT Unit Effect)
(Either (NonEmptyList ForeignError) CompileResult)
compile endpoint code = ExceptT (ContT \k -> runEffectFn4 compile_ endpoint code (mkEffectFn1 (k <<< Right <<< runExcept <<< decode)) (mkEffectFn1 (k <<< Left)))
get :: URL -> ExceptT String Aff String
get url = ExceptT $ AX.get AXRF.string url >>= case _ of
Left e ->
pure $ Left $ printError e
Right { status } | status >= StatusCode 400 ->
pure $ Left $ "Received error status code: " <> show status
Right { body } ->
pure $ Right body
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the second check (which guards against status codes over 400) is technically unnecessary, but I couldn't verify that at a quick breeze through the Affjax documentation so I've included it here. I can remove these checks if they're redundant, though.

This doesn't really need to be in ExceptT, but doing so allows me to minimize the diff everywhere else.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you check and see what happens if you get a 404 status code? I haven't been able to verify from a quick glance through the documentation either but I actually suspect it's not redundant. Either way, I think we should know the answer before merging.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ll verify this check is necessary. Do you have any opinion on the code as-is if the check does turn out to be necessary?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code seems fine to me as-is if the check is necessary. If it's not necessary, I think we should remove it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hdgarrood Yep, it turns out this check is necessary.

If the request is made with a malformed URL:
Screen Shot 2020-12-15 at 6 14 49 PM

If the request returns a 404:
Screen Shot 2020-12-15 at 6 17 01 PM


-- | POST the specified code to the Try PureScript API, and wait for a response.
compile :: String -> String -> ExceptT String Aff (Either (NonEmptyList ForeignError) CompileResult)
compile endpoint code = ExceptT $ AX.post AXRF.json (endpoint <> "/compile") (Just requestBody) >>= case _ of
Left e ->
pure $ Left $ printError e
Right { status } | status >= StatusCode 400 ->
pure $ Left $ "Received error status code: " <> show status
Right { body } ->
pure $ Right $ runExcept (decode (unsafeToForeign body))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This unsafeToForeign is for parity with the previous FFI code, which specified a JSON return type (in jQuery) and then decodes it as Foreign (from PureScript).

where
requestBody = AXRB.String code
45 changes: 3 additions & 42 deletions client/src/Try/Gist.js
Original file line number Diff line number Diff line change
@@ -1,48 +1,9 @@
"use strict";

exports.getGistById_ = function(id, done, fail) {
$.ajax({
url: 'https://api.github.com/gists/' + id,
dataType: 'json'
}).done(done).fail(function(err) {
fail("Unable to load Gist metadata");
});
}

exports.tryLoadFileFromGist_ = function(gistInfo, filename, done, fail) {
exports.rawUrl_ = function (gistInfo, filename) {
Copy link
Member Author

@thomashoneyman thomashoneyman Dec 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This small function, rawUrl_, is copied from the beginning of tryLoadFileFromGist_:

exports.tryLoadFileFromGist_ = function(gistInfo, filename, done, fail) {
  if (gistInfo.files && gistInfo.files.hasOwnProperty(filename)) {
    var url = gistInfo.files[filename].raw_url;

I opted to keep this code in the FFI rather than implement new data types and decoding in PureScript. That's once again to keep the diff minimal. In the future, though, it would be nice to switch to something like codec-argonaut instead. You can see this function used in tryLoadFileFromGist in the PureScript code.

if (gistInfo.files && gistInfo.files.hasOwnProperty(filename)) {
var url = gistInfo.files[filename].raw_url;

return $.ajax({
url: url,
dataType: 'text'
}).done(done).fail(function(err) {
fail(err.statusText);
});
return gistInfo.files[filename].raw_url;
} else {
fail("Gist does not contain a file named " + filename);
return null;
}
};

exports.uploadGist_ = function(content, done, fail) {
var data = {
"description": "Published with try.purescript.org",
"public": false,
"files": {
"Main.purs": {
"content": content
}
}
};

$.ajax({
url: 'https://api.github.com/gists',
type: 'POST',
dataType: 'json',
data: JSON.stringify(data)
}).success(function(e) {
done(e.id);
}).error(function(e) {
fail(e);
});
};
Loading