One of the fields in my datatype for a table in my PostgreSQL database is a newtype wrapping UUID called ItemId.
import Data.Profunctor.Product.TH (makeAdaptorAndInstance)
import Data.DateTime (DateTime)
import Data.UUID
import GHC.Generics
import qualified Opaleye as O
import Data.Text (pack, Text)
newtype ItemId = ItemId UUID
deriving (Show, Eq, Generic)
toItemId :: UUID -> ItemId
toItemId = ItemId
fromItemId :: ItemId -> UUID
fromItemId (ItemId x) = x
data Item' id name desc num most
= Item {
_itemId :: id,
_itemName :: name,
_itemDesc :: desc,
_numTimesOrdered :: num,
_mostRecentOrder :: most
}
type ItemRead = Item' ItemId Text Text Int DateTime
type ItemWrite = Item' (Maybe ItemId) Text Text (Maybe Int) (Maybe DateTime)
type ItemColRead = Item' (O.Column O.PGUuid)
(O.Column O.PGText)
(O.Column O.PGText)
(O.Column O.PGInt4)
(O.Column O.PGTimestamptz)
type ItemColWrite = Item' (Maybe (O.Column O.PGUuid))
(O.Column O.PGText)
(O.Column O.PGText)
(Maybe (O.Column O.PGInt4))
(Maybe (O.Column O.PGTimestamptz))
$(makeAdaptorAndInstance "pItem" ''Item')
itemTable :: O.Table ItemColWrite ItemColRead
itemTable = O.Table "items" (pItem Item { _itemId = O.optional "id"
, _itemName = O.required "name"
, _itemDesc = O.required "desc"
, _numTimesOrdered = O.optional "numTimesOrdered"
, _mostRecentOrder = O.optional "mostRecentOrder"
})
itemToPG :: ItemWrite -> ItemColWrite
itemToPG = pItem Item { _itemId = const Nothing
, _itemName = O.pgStrictText
, _itemDesc = O.pgStrictText
, _numTimesOrdered = const Nothing
, _mostRecentOrder = const Nothing
}
However, when I compile my project, GHC throws:
/home/gigavinyl/Projects/ordermage/src/Api/Item.hs:34:3: error:
• No instance for (O.QueryRunnerColumnDefault O.PGUuid ItemId)
arising from a use of ‘O.runInsertManyReturning’
• In the second argument of ‘(<$>)’, namely
‘O.runInsertManyReturning con itemTable [itemToPG item] _itemId’
In the second argument of ‘($)’, namely
‘listToMaybe
<$> O.runInsertManyReturning con itemTable [itemToPG item] _itemId’
In the expression:
liftIO
$ listToMaybe
<$> O.runInsertManyReturning con itemTable [itemToPG item] _itemId
where src/Api/Item.hs is:
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE OverloadedStrings #-}
module Api.Item where
import Control.Monad.IO.Class (liftIO)
import Data.Maybe (listToMaybe)
import Database.PostgreSQL.Simple (Connection)
import Models.Item
import Queries.Item
import Servant
import qualified Opaleye as O
type ItemApi =
Get '[JSON] [ItemRead] :<|>
Capture "itemId" ItemId :> Get '[JSON] (Maybe ItemRead) :<|>
ReqBody '[JSON] ItemWrite :> Post '[JSON] (Maybe ItemId)
itemServer :: Connection -> Server ItemApi
itemServer con =
getItems con :<|>
getItemById con :<|>
postItem con
getItems :: Connection -> Handler [ItemRead]
getItems con = liftIO $ O.runQuery con itemsQuery
getItemById :: Connection -> ItemId -> Handler (Maybe ItemRead)
getItemById con itemID = liftIO $ listToMaybe <$> O.runQuery con (itemByIdQuery itemID)
postItem :: Connection -> ItemWrite -> Handler (Maybe ItemId)
postItem con item = liftIO $ listToMaybe <$>
O.runInsertManyReturning con itemTable [itemToPG item] _itemId
I'm still fairly new to Haskell but the issue appears to be that Opaleye doesn't know how to convert ItemId into a PGUuid but I know it can convert UUID to PGUuid. How would I go about writing the instance to allow Opaleye to do this conversion?
the issue appears to be that Opaleye doesn't know how to convert ItemId into a PGUuid but I know it can convert UUID to PGUuid
It's the other way round. It's trying to convert a Column PGUuid into an ItemId and it only knows how to convert it into a UUID. One approach is to add the instance yourself:
instance O.QueryRunnerColumnDefault O.PGUuid ItemId where
queryRunnerColumnDefault =
O.queryRunnerColumn id ItemId queryRunnerColumnDefault
Another approach would be to make ItemId polymorphic:
newtype ItemId' a = ItemId a
$(makeAdaptorAndInstance "pItemId" ''ItemId')
and then you can use it on both the Haskell side and Opaleye side without having to write the extra instance.
Related
Consider the following code sample, which creates a new type to represent a customer model:
module Main where
import Effect (Effect)
import Effect.Console ( logShow )
import Prelude (Unit,(+),(/),(*),(-), (<>),discard)
newtype Customer
= Customer
{ firstname :: String
}
sample :: Customer
sample = Customer
{ firstname : "Average"
}
first :: Customer -> String
first a = _.firstname a
main = do
logShow ( first sample )
The expected output would be the value Average, which is equal to sample.name, but instead an error is produced:
Could not match type
{ firstname :: t0
| t1
}
with type
Customer
while checking that type Customer
is at least as general as type { firstname :: t0
| t1
}
while checking that expression a
has type { firstname :: t0
| t1
}
in value declaration first
where t0 is an unknown type
t1 is an unknown type
This is a good error, but doesn't explain how to actually access this value.
How do you access the value of an object created as a newType?
You have to do
first :: Customer -> String
first (Customer a) = _.firstname a
Since newtype is really, a new type.
One another way is to derive Newtype instance for that particular newtype, which exposes certain functions that will let you work on the data wrapped by the newtype.
derive instance newtypeCustomer :: Newtype Customer _
first :: Customer -> String
first = (_.firstname <<< unwrap)
I have input fields which I have marked with a required attribute, but can't figure out a way to trigger a validation check (I am not working inside of a form, so using a default submit button action won't work for me).
A quick pursuit search shows many validity functions for core html element types, but I'm not sure how to apply these to Halogen.
Is there some way to trigger a DOM effect to check all required inputs on the page and get a result back?
Here is an example component showing what I'm trying to achieve
import Prelude
import Data.Maybe (Maybe(..))
import Halogen as H
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.HTML.Properties as HP
data Message = Void
type State =
{ textValue :: String
, verified :: Boolean
}
data Query a = ContinueClicked a | InputEntered String a
inputHtml :: State -> H.ComponentHTML Query
inputHtml state =
HH.div [ HP.class_ $ H.ClassName "input-div" ]
[ HH.label_ [ HH.text "This is a required field" ]
, HH.input [ HP.type_ HP.InputText
, HE.onValueInput $ HE.input InputEntered
, HP.value state.textValue
, HP.required true
]
, HH.button [ HE.onClick $ HE.input_ ContinueClicked ]
[ HH.text "Continue"]
]
verifiedHtml :: H.ComponentHTML Query
verifiedHtml =
HH.div_ [ HH.h3_ [ HH.text "Verified!" ] ]
render :: State -> H.ComponentHTML Query
render state = if state.verified then verifiedHtml else inputHtml state
eval :: forall m. Query ~> H.ComponentDSL State Query Message m
eval = case _ of
InputEntered v next -> do
H.modify $ (_ { textValue = v })
pure next
ContinueClicked next -> do
let inputValid = false -- somehow use the required prop to determine if valid
when inputValid $ H.modify $ (_ { verified = true })
pure next
initialState :: State
initialState =
{ textValue : ""
, verified : false
}
component :: forall m. H.Component HH.HTML Query Unit Message m
component =
H.component
{ initialState: const initialState
, render
, eval
, receiver: const Nothing
}
I don't think relying on HTML form validation is the most effective way of checking inputs within a Halogen application. But I'll assume you have your reasons and present an answer anyway.
First things first, if we want to deal with DOM elements we need a way to retrieve them. Here's a purescript version of document.getElementById
getElementById
:: forall a eff
. (Foreign -> F a)
-> String
-> Eff (dom :: DOM | eff) (Maybe a)
getElementById reader elementId =
DOM.window
>>= DOM.document
<#> DOM.htmlDocumentToNonElementParentNode
>>= DOM.getElementById (wrap elementId)
<#> (_ >>= runReader reader)
runReader :: forall a b. (Foreign -> F b) -> a -> Maybe b
runReader r =
hush <<< runExcept <<< r <<< toForeign
(Don't worry about the new imports for now, there's a complete module at the end)
This getElementById function takes a read* function (probably from DOM.HTML.Types) to determine the type of element you get back, and an element id as a string.
In order to use this, we need to add an extra property to your HH.input:
HH.input [ HP.type_ HP.InputText
, HE.onValueInput $ HE.input InputEntered
, HP.value state.textValue
, HP.required true
, HP.id_ "myInput" <-- edit
]
Aside: a sum type with a Show instance would be safer than hard-coding stringy ids everywhere. I'll leave that one to you.
Cool. Now we need to call this from the ContinueClicked branch of your eval function:
ContinueClicked next ->
do maybeInput <- H.liftEff $
getElementById DOM.readHTMLInputElement "myInput"
...
This gives us a Maybe HTMLInputElement to play with. And that HTMLInputElement should have a validity property of type ValidityState, which has the information we're after.
DOM.HTML.HTMLInputElement has a validity function that will give us access to that property. Then we'll need to do some foreign value manipulation to try and get the data out that we want. For simplicity, let's just try and pull out the valid field:
isValid :: DOM.ValidityState -> Maybe Boolean
isValid =
runReader (readProp "valid" >=> readBoolean)
And with that little helper, we can finish the ContinueClicked branch:
ContinueClicked next ->
do maybeInput <- H.liftEff $
getElementById DOM.readHTMLInputElement "myInput"
pure next <*
case maybeInput of
Just input ->
do validityState <- H.liftEff $ DOM.validity input
when (fromMaybe false $ isValid validityState) $
H.modify (_ { verified = true })
Nothing ->
H.liftEff $ log "myInput not found"
And then putting it all together we have...
module Main where
import Prelude
import Control.Monad.Aff (Aff)
import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Console (CONSOLE, log)
import Control.Monad.Except (runExcept)
import Data.Either (hush)
import Data.Foreign (Foreign, F, toForeign, readBoolean)
import Data.Foreign.Index (readProp)
import Data.Maybe (Maybe(..), fromMaybe)
import Data.Newtype (wrap)
import DOM (DOM)
import DOM.HTML (window) as DOM
import DOM.HTML.HTMLInputElement (validity) as DOM
import DOM.HTML.Types
(ValidityState, htmlDocumentToNonElementParentNode, readHTMLInputElement) as DOM
import DOM.HTML.Window (document) as DOM
import DOM.Node.NonElementParentNode (getElementById) as DOM
import Halogen as H
import Halogen.Aff as HA
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.HTML.Properties as HP
import Halogen.VDom.Driver (runUI)
main :: Eff (HA.HalogenEffects (console :: CONSOLE)) Unit
main = HA.runHalogenAff do
body <- HA.awaitBody
runUI component unit body
type Message
= Void
type Input
= Unit
type State
= { textValue :: String
, verified :: Boolean
}
data Query a
= ContinueClicked a
| InputEntered String a
component
:: forall eff
. H.Component HH.HTML Query Unit Message (Aff (console :: CONSOLE, dom :: DOM | eff))
component =
H.component
{ initialState: const initialState
, render
, eval
, receiver: const Nothing
}
initialState :: State
initialState =
{ textValue : ""
, verified : false
}
render :: State -> H.ComponentHTML Query
render state =
if state.verified then verifiedHtml else inputHtml
where
verifiedHtml =
HH.div_ [ HH.h3_ [ HH.text "Verified!" ] ]
inputHtml =
HH.div
[ HP.class_ $ H.ClassName "input-div" ]
[ HH.label_ [ HH.text "This is a required field" ]
, HH.input
[ HP.type_ HP.InputText
, HE.onValueInput $ HE.input InputEntered
, HP.value state.textValue
, HP.id_ "myInput"
, HP.required true
]
, HH.button
[ HE.onClick $ HE.input_ ContinueClicked ]
[ HH.text "Continue" ]
]
eval
:: forall eff
. Query
~> H.ComponentDSL State Query Message (Aff (console :: CONSOLE, dom :: DOM | eff))
eval = case _ of
InputEntered v next ->
do H.modify (_{ textValue = v })
pure next
ContinueClicked next ->
do maybeInput <- H.liftEff $
getElementById DOM.readHTMLInputElement "myInput"
pure next <*
case maybeInput of
Just input ->
do validityState <- H.liftEff $ DOM.validity input
when (fromMaybe false $ isValid validityState) $
H.modify (_ { verified = true })
Nothing ->
H.liftEff $ log "myInput not found"
getElementById
:: forall a eff
. (Foreign -> F a)
-> String
-> Eff (dom :: DOM | eff) (Maybe a)
getElementById reader elementId =
DOM.window
>>= DOM.document
<#> DOM.htmlDocumentToNonElementParentNode
>>= DOM.getElementById (wrap elementId)
<#> (_ >>= runReader reader)
isValid :: DOM.ValidityState -> Maybe Boolean
isValid =
runReader (readProp "valid" >=> readBoolean)
runReader :: forall a b. (Foreign -> F b) -> a -> Maybe b
runReader r =
hush <<< runExcept <<< r <<< toForeign
I am new to Purescript and I am trying to write a function that
can take any record value and iterate over the fields and values and build
a querystring.
I am thinking something like:
buildQueryString :: forall a. PropertyTraversible r => r -> String
which I want to use like this:
buildQueryString {name: "joe", age: 10} -- returns: "name=joe&age=10"
Is there a way to write something like that in Purescript with existing idioms or do I have to create my own custom Type Class for this?
I'm sure that it can be shorter, but here is my implementation based on purescript-generic-rep (inspired by genericShow). This solution uses typeclasses - it seems to be standard approach with generic-rep:
module Main where
import Prelude
import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Console (CONSOLE, log)
import Data.Foldable (intercalate)
import Data.Generic.Rep (class Generic, Constructor(..), Field(..), Product(..), Rec(..), from)
import Data.Symbol (class IsSymbol, SProxy(..), reflectSymbol)
class EncodeValue a where
encodeValue ∷ a → String
instance encodeValueString ∷ EncodeValue String where
encodeValue = id
instance encodeValueInt ∷ EncodeValue Int where
encodeValue = show
class EncodeFields a where
encodeFields :: a -> Array String
instance encodeFieldsProduct
∷ (EncodeFields a, EncodeFields b)
⇒ EncodeFields (Product a b) where
encodeFields (Product a b) = encodeFields a <> encodeFields b
instance encodeFieldsField
∷ (EncodeValue a, IsSymbol name)
⇒ EncodeFields (Field name a) where
encodeFields (Field a) =
[reflectSymbol (SProxy :: SProxy name) <> "=" <> encodeValue a]
buildQueryString
∷ ∀ a l n.
Generic n (Constructor l (Rec a))
⇒ (EncodeFields a)
⇒ n
→ String
buildQueryString n =
build <<< from $ n
where
build (Constructor (Rec fields)) = intercalate "&" <<< encodeFields $ fields
newtype Person =
Person
{ name ∷ String
, age ∷ Int
}
derive instance genericPerson ∷ Generic Person _
joe ∷ Person
joe = Person { name: "joe", age: 10 }
main :: forall e. Eff (console :: CONSOLE | e) Unit
main = do
log <<< buildQueryString $ joe
buildQueryString expects value of type with single constructor which contains a record (possibly just newtype) because it is impossible to derive a Generic instance for "unwrapped" Record type.
If you want to handle also Array values etc. then encodeValue should probably return values of type Array String.
This is possible with purescript-generics but it only works on nominal types, not on any record. But it saves you boilerplate, since you can just derive the instance for Generic, so it would work with any data or newtype without further modification.
Downside is, you have to make some assumptions about the type: like it only contains one record and the record does not contain arrays or other records.
Here is a hacky demonstration how it would work:
data Person = Person
{ name :: String
, age :: Int
}
derive instance genericPerson :: Generic Person
joe = Person { name: "joe", age: 10 }
build :: GenericSpine -> String
build (SRecord arr) = intercalate "&" (map (\x -> x.recLabel <> "=" <> build (x.recValue unit)) arr)
build (SProd _ arr) = fromMaybe "TODO" $ map (\f -> build (f unit)) (head arr)
build (SString s) = s
build (SInt i) = show i
build _ = "TODO"
test = build (toSpine joe)
purescript-generics-rep is newer, so possibly there is a better solution, maybe even on any record. I have not tried that (yet).
For example javascript library has this hierarchy
class Base
class Foo:Base
class Bar:Base
and this function
calc(x:Base) : Int
calc(new Bar())
How do you write this function in PureScript?
foreign import calc :: ??? -> Int
I think it depends on what do you want to do with these classes. I would do something like this:
-- purs file
foreign import data Base :: *
foreign import data Foo :: *
foreign import data Bar :: *
fooToBase :: Foo -> Base
fooToBase = unsafeCoerce
barToBase :: Bar -> Base
barToBase = unsafeCoerce
foreign import newFoo :: forall e. Eff e Foo
foreign import newBar :: forall e. Eff e Bar
-- works with all ancestors
foreign import calc :: Base -> Eff e Unit
-- works only with Foos
foreign import fooMethod :: String -> Foo -> Eff e Int
-- using
main = do
foo <- newFoo
bar <- newBar
calc $ fooToBase foo
calc $ barToBase bar
fooMethod "test" foo
-- js file
exports.newFoo = function() { return new Foo(); };
exports.newBar = function() { return new Bar(); };
exports.calc = function(o) {
return function() {
return o.calc();
};
};
exports.fooMethod = function(str) {
return function(o) {
return function() {
return o.fooMethod();
};
};
};
Everything here should live in Eff probably, because making new instances changes global state.
Assume I have User records in my PureScript code with the following type:
{ id :: Number
, username :: String
, email :: Maybe String
, isActive :: Boolean
}
A CommonJS module is derived from the PureScript code. Exported User-related functions will be called from external JavaScript code.
In the JavaScript code, a "user" may be represented as:
var alice = {id: 123, username: 'alice', email: 'alice#example.com', isActive: true};
email may be null:
var alice = {id: 123, username: 'alice', email: null, isActive: true};
email may be omitted:
var alice = {id: 123, username: 'alice', isActive: true};
isActive may be omitted, in which case it is assumed true:
var alice = {id: 123, username: 'alice'};
id is unfortunately sometimes a numeric string:
var alice = {id: '123', username: 'alice'};
The five JavaScript representations above are equivalent and should produce equivalent PureScript records.
How do I go about writing a function which takes a JavaScript object and returns a User record? It would use the default value for a null/omitted optional field, coerce a string id to a number, and throw if a required field is missing or if a value is of the wrong type.
The two approaches I can see are to use the FFI in the PureScript module or to define the conversion function in the external JavaScript code. The latter seems hairy:
function convert(user) {
var rec = {};
if (user.email == null) {
rec.email = PS.Data_Maybe.Nothing.value;
} else if (typeof user.email == 'string') {
rec.email = PS.Data_Maybe.Just.create(user.email);
} else {
throw new TypeError('"email" must be a string or null');
}
// ...
}
I'm not sure how the FFI version would work. I haven't yet worked with effects.
I'm sorry that this question is not very clear. I don't yet have enough understanding to know exactly what it is that I want to know.
I've put together a solution. I'm sure much can be improved, such as changing the type of toUser to Json -> Either String User and preserving error information. Please leave a comment if you can see any ways this code could be improved. :)
This solution uses PureScript-Argonaut in addition to a few core modules.
module Main
( User()
, toEmail
, toId
, toIsActive
, toUser
, toUsername
) where
import Control.Alt ((<|>))
import Data.Argonaut ((.?), toObject)
import Data.Argonaut.Core (JNumber(), JObject(), Json())
import Data.Either (Either(..), either)
import Data.Maybe (Maybe(..))
import Global (isNaN, readFloat)
type User = { id :: Number
, username :: String
, email :: Maybe String
, isActive :: Boolean
}
hush :: forall a b. Either a b -> Maybe b
hush = either (const Nothing) Just
toId :: JObject -> Maybe Number
toId obj = fromNumber <|> fromString
where
fromNumber = (hush $ obj .? "id")
fromString = (hush $ obj .? "id") >>= \s ->
let id = readFloat s in if isNaN id then Nothing else Just id
toUsername :: JObject -> Maybe String
toUsername obj = hush $ obj .? "username"
toEmail :: JObject -> Maybe String
toEmail obj = hush $ obj .? "email"
toIsActive :: JObject -> Maybe Boolean
toIsActive obj = (hush $ obj .? "isActive") <|> Just true
toUser :: Json -> Maybe User
toUser json = do
obj <- toObject json
id <- toId obj
username <- toUsername obj
isActive <- toIsActive obj
return { id: id
, username: username
, email: toEmail obj
, isActive: isActive
}
Update: I've made improvements to the code above based on a gist from Ben Kolera.
Have you had a look at purescript-foreign (https://github.com/purescript/purescript-foreign)? I think that's what you're looking for here.
As gb. wrote, that is exactly what the Foreign data type was built for. Off the top of my head:
convert :: Foreign -> F User
convert f = do
id <- f ! "id" >>= readNumber
name <- f ! "name" >>= readString
email <- (f ! "email" >>= readNull >>= traverse readString) <|> pure Nothing
isActive <- (f ! "isActive" >>= readBoolean) <|> pure true
return { id, name, email, isActive }
Just a little more ffi
module User where
import Data.Maybe
import Data.Function
foreign import data UserExternal :: *
type User =
{
id :: Number,
username :: String,
email :: Maybe String,
isActive :: Boolean
}
type MbUser =
{
id :: Maybe Number,
username :: Maybe String,
email :: Maybe String,
isActive :: Maybe Boolean
}
foreign import toMbUserImpl """
function toMbUserImpl(nothing, just, user) {
var result = {},
properties = ['username', 'email', 'isActive'];
var i, prop;
for (i = 0; i < properties.length; i++) {
prop = properties[i];
if (user.hasOwnProperty(prop)) {
result[prop] = just(user[prop]);
} else {
result[prop] = nothing;
}
}
if (!user.hasOwnProperty('id') || isNaN(parseInt(user.id))) {
result.id = nothing;
} else {
result.id = just(user.id);
}
return result;
}
""" :: forall a. Fn3 (Maybe a) (a -> Maybe a) UserExternal MbUser
toMbUser :: UserExternal -> MbUser
toMbUser ext = runFn3 toMbUserImpl Nothing Just ext
defaultId = 0
defaultName = "anonymous"
defaultActive = false
userFromMbUser :: MbUser -> User
userFromMbUser mbUser =
{
id: fromMaybe defaultId mbUser.id,
username: fromMaybe defaultName mbUser.username,
email: mbUser.email,
isActive: fromMaybe defaultActive mbUser.isActive
}
userFromExternal :: UserExternal -> User
userFromExternal ext = userFromMbUser $ toMbUser ext