Correction stats coded, but not yet used online

This commit is contained in:
Steffen Jost 2019-06-14 17:05:45 +02:00
parent 9b7d63d469
commit 718a2b026c
8 changed files with 248 additions and 16 deletions

5
.vscode/tasks.json vendored
View File

@ -58,6 +58,11 @@
"type": "npm", "type": "npm",
"script": "start", "script": "start",
"problemMatcher": [] "problemMatcher": []
},
{
"type": "npm",
"script": "frontend:lint",
"problemMatcher": []
} }
] ]
} }

View File

@ -394,14 +394,21 @@ UpdatedAssignedCorrectorsAuto num@Int64: #{display num} Abgaben wurden unter den
CouldNotAssignCorrectorsAuto num@Int64: #{display num} Abgaben konnten nicht automatisch zugewiesen werden: CouldNotAssignCorrectorsAuto num@Int64: #{display num} Abgaben konnten nicht automatisch zugewiesen werden:
SelfCorrectors num@Int64: #{display num} Abgaben wurden Abgebenden als eigenem Korrektor zugeteilt! SelfCorrectors num@Int64: #{display num} Abgaben wurden Abgebenden als eigenem Korrektor zugeteilt!
CorrectionSheets: Übersicht Korrekturen nach Blättern
CorrectionCorrectors: Übersicht Korrekturen nach Korrektoren
AssignSubmissionExceptionNoCorrectors: Es sind keine Korrektoren eingestellt AssignSubmissionExceptionNoCorrectors: Es sind keine Korrektoren eingestellt
AssignSubmissionExceptionNoCorrectorsByProportion: Es sind keine Korrektoren mit Anteil ungleich Null eingestellt AssignSubmissionExceptionNoCorrectorsByProportion: Es sind keine Korrektoren mit Anteil ungleich Null eingestellt
AssignSubmissionExceptionSubmissionsNotFound n@Int: #{tshow n} Abgaben konnten nicht gefunden werden AssignSubmissionExceptionSubmissionsNotFound n@Int: #{tshow n} Abgaben konnten nicht gefunden werden
NrSubmittorsTotal: Abgebende
NrSubmissionsTotal: Abgaben NrSubmissionsTotal: Abgaben
NrSubmissionsUnassigned: Ohne Korrektor NrSubmissionsUnassigned: Ohne Korrektor
NoCorrectorAssigned: Ohne Korrektor
NrCorrectors: Korrektoren NrCorrectors: Korrektoren
NrSubmissionsNewlyAssigned: Neu zugeteilt NrSubmissionsNewlyAssigned: Neu zugeteilt
NrSubmissionsNotAssigned: Nicht zugeteilt NrSubmissionsNotAssigned: Nicht zugeteilt
NrSubmissionsNotCorrected: Unkorrigiert
CorrectionTime: Korrekturdauer (Min/Avg/Max)
CorrectionsUploaded num@Int64: #{display num} Korrekturen wurden gespeichert: CorrectionsUploaded num@Int64: #{display num} Korrekturen wurden gespeichert:
NoCorrectionsUploaded: In der hochgeladenen Datei wurden keine Korrekturen gefunden. NoCorrectionsUploaded: In der hochgeladenen Datei wurden keine Korrekturen gefunden.
@ -971,7 +978,7 @@ TutorialDelete: Löschen
CourseTutorials: Übungen CourseTutorials: Übungen
ParticipantsN n@Int: Teilnehmer ParticipantsN n@Int: #{tshow n} Teilnehmer
TutorialDeleteQuestion: Wollen Sie das unten aufgeführte Tutorium wirklich löschen? TutorialDeleteQuestion: Wollen Sie das unten aufgeführte Tutorium wirklich löschen?
TutorialDeleted: Tutorium gelöscht TutorialDeleted: Tutorium gelöscht
@ -1007,6 +1014,7 @@ HealthLDAPAdmins: Anteil der Administratoren, die im LDAP-Verzeichnis gefunden w
HealthSMTPConnect: SMTP-Server kann erreicht werden HealthSMTPConnect: SMTP-Server kann erreicht werden
HealthWidgetMemcached: Memcached-Server liefert Widgets korrekt aus HealthWidgetMemcached: Memcached-Server liefert Widgets korrekt aus
CourseParticipants n@Int: Derzeit #{tshow n} angemeldete Kursteilnehmer
CourseParticipantsInvited n@Int: #{tshow n} #{pluralDE n "Einladung" "Einladungen"} per E-Mail verschickt CourseParticipantsInvited n@Int: #{tshow n} #{pluralDE n "Einladung" "Einladungen"} per E-Mail verschickt
CourseParticipantsAlreadyRegistered n@Int: #{tshow n} Teilnehmer #{pluralDE n "ist" "sind"} bereits angemeldet CourseParticipantsAlreadyRegistered n@Int: #{tshow n} Teilnehmer #{pluralDE n "ist" "sind"} bereits angemeldet
CourseParticipantsRegisteredWithoutField n@Int: #{tshow n} Teilnehmer #{pluralDE n "wurde ohne assoziiertes Hauptfach" "wurden assoziierte Hauptfächer"} angemeldet, da #{pluralDE n "kein eindeutiges Hauptfach bestimmt werden konnte" "keine eindeutigen Hauptfächer bestimmt werden konnten"} CourseParticipantsRegisteredWithoutField n@Int: #{tshow n} Teilnehmer #{pluralDE n "wurde ohne assoziiertes Hauptfach" "wurden assoziierte Hauptfächer"} angemeldet, da #{pluralDE n "kein eindeutiges Hauptfach bestimmt werden konnte" "keine eindeutigen Hauptfächer bestimmt werden konnten"}

View File

@ -4,7 +4,7 @@ Sheet -- exercise sheet for a given course
description Html Maybe description Html Maybe
type SheetType -- Does it count towards overall course grade? type SheetType -- Does it count towards overall course grade?
grouping SheetGroup -- May participants submit in groups of certain sizes? grouping SheetGroup -- May participants submit in groups of certain sizes?
markingText Html Maybe -- Instructions for correctors, included in marking templates markingText Html Maybe -- Instructons for correctors, included in marking templates
visibleFrom UTCTime Maybe -- Invisible to enrolled participants before visibleFrom UTCTime Maybe -- Invisible to enrolled participants before
activeFrom UTCTime -- Download of questions and submission is permitted afterwards activeFrom UTCTime -- Download of questions and submission is permitted afterwards
activeTo UTCTime -- Submission is only permitted before activeTo UTCTime -- Submission is only permitted before

View File

@ -5,6 +5,7 @@ import Import
import Jobs import Jobs
import Handler.Utils import Handler.Utils
import Handler.Utils.Corrections
import Handler.Utils.Submission import Handler.Utils.Submission
import Handler.Utils.Table.Cells import Handler.Utils.Table.Cells
import Handler.Utils.SheetType import Handler.Utils.SheetType
@ -13,11 +14,11 @@ import Handler.Utils.Delete
import Utils.Lens import Utils.Lens
import Data.List (nub) import Data.List as List (nub, foldl, foldr)
import Data.Set (Set) import Data.Set (Set)
import qualified Data.Set as Set import qualified Data.Set as Set
import Data.Map (Map, (!)) import Data.Map.Strict (Map, (!))
import qualified Data.Map as Map import qualified Data.Map.Strict as Map
import qualified Data.Text as Text import qualified Data.Text as Text
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
@ -250,7 +251,7 @@ makeCorrectionsTable whereClause dbtColonnade dbtFilterUI psValidator dbtProj' d
E.orderBy [E.asc $ user E.^. UserSurname, E.asc $ user E.^. UserDisplayName] E.orderBy [E.asc $ user E.^. UserSurname, E.asc $ user E.^. UserDisplayName]
return (user, pseudonym E.?. SheetPseudonymPseudonym) return (user, pseudonym E.?. SheetPseudonymPseudonym)
let let
submittorMap = foldr (\(Entity userId user, E.Value pseudo) -> Map.insert userId (user, pseudo)) Map.empty submittors submittorMap = List.foldr (\(Entity userId user, E.Value pseudo) -> Map.insert userId (user, pseudo)) Map.empty submittors
dbtProj' (submission, sheet, (courseName, courseShorthand, courseTerm, courseSchool), mCorrector, mbLastEdit, submittorMap) dbtProj' (submission, sheet, (courseName, courseShorthand, courseTerm, courseSchool), mCorrector, mbLastEdit, submittorMap)
dbTable psValidator DBTable dbTable psValidator DBTable
{ dbtSQLQuery { dbtSQLQuery
@ -1035,7 +1036,7 @@ embedRenderMessage ''UniWorX ''ButtonSubmissionsAssign id
instance Button UniWorX ButtonSubmissionsAssign where instance Button UniWorX ButtonSubmissionsAssign where
btnClasses BtnSubmissionsAssign = [BCIsButton, BCPrimary] btnClasses BtnSubmissionsAssign = [BCIsButton, BCPrimary]
-- | Gather info about corrector assignment per sheet -- | DEPRECATED use CorrectorInfo instead. Gather info about corrector assignment per sheet
data SubAssignInfo = SubAssignInfo { saiName :: SheetName, saiSubmissionNr, saiCorrectorNr, saiUnassignedNr :: Int } data SubAssignInfo = SubAssignInfo { saiName :: SheetName, saiSubmissionNr, saiCorrectorNr, saiUnassignedNr :: Int }
getCAssignR, postCAssignR :: TermId -> SchoolId -> CourseShorthand -> Handler Html getCAssignR, postCAssignR :: TermId -> SchoolId -> CourseShorthand -> Handler Html
@ -1092,3 +1093,134 @@ assignHandler tid ssh csh rawSids = do
then linkBack -- TODO: convenience link might be unnecessary: either via breadcrumb or closing model. Remove, if modal works fine. Otherwise change to PrimaryAction? then linkBack -- TODO: convenience link might be unnecessary: either via breadcrumb or closing model. Remove, if modal works fine. Otherwise change to PrimaryAction?
else btnForm else btnForm
assignHandler' :: TermId -> SchoolId -> CourseShorthand -> [SheetId] -> Handler Html
assignHandler' tid ssh csh _rawSids = do
-- gather data
(nrParticipants, groupsPossible, infoMap, correctorMap) <- runDB $ do
cid <- getKeyBy404 $ TermSchoolCourseShort tid ssh csh
nrParticipants <- count [CourseParticipantCourse ==. cid]
sheetList <- selectList [SheetCourse ==. cid] [Asc SheetName]
let sheets = entities2map sheetList
sheetIds = Map.keys sheets
groupsPossible :: Bool
groupsPossible =
let foldFun (Entity _ Sheet{sheetGrouping=sgr}) acc = acc || sgr /= NoGroups
in List.foldr foldFun False sheetList
correctors <- E.select . E.from $ \(corrector `E.InnerJoin` user) -> do
E.on $ corrector E.^. SheetCorrectorUser E.==. user E.^. UserId
E.where_ $ corrector E.^. SheetCorrectorSheet `E.in_` E.valList sheetIds
return (corrector, user)
let correctorMap :: Map UserId (SheetCorrector,User)
correctorMap = foldl (\m (Entity _ corr, Entity uid user)-> Map.insert uid (corr,user) m) Map.empty correctors
submissions <- E.select . E.from $ \submission -> do
E.where_ $ submission E.^. SubmissionSheet `E.in_` E.valList sheetIds
let numSubmittors = E.sub_select . E.from $ \subUser -> do
E.where_ $ submission E.^. SubmissionId E.==. subUser E.^. SubmissionUserSubmission
return E.countRows
return (submission, numSubmittors)
-- prepare map
let infoMap :: Map SheetName (Map (Maybe UserId) CorrectionInfo)
infoMap = List.foldl (flip buildS) emptySheets submissions
-- ensure that all sheets are shown, including those without any submissions
emptySheets = foldl (\m sid -> Map.insert (sheetName $ sheets ! sid) Map.empty m) Map.empty sheetIds
buildS :: (Entity Submission, E.Value Int64) -> Map SheetName (Map (Maybe UserId) CorrectionInfo) -> Map SheetName (Map (Maybe UserId) CorrectionInfo)
buildS (Entity _sheetId Submission{..}, E.Value nrSbmtrs) m =
let shnm = sheetName $ sheets ! submissionSheet
corTime = diffUTCTime <$> submissionRatingTime <*> submissionRatingAssigned
cinf = Map.singleton submissionRatingBy $ CorrectionInfo
{ ciSubmittors = fromIntegral nrSbmtrs
, ciSubmissions = 1
, ciAssigned = maybe 0 (const 1) submissionRatingBy
, ciCorrected = maybe 0 (const 1) submissionRatingTime
, ciCorrector = submissionRatingBy
, ciMin = corTime
, ciTot = corTime
, ciMax = corTime
}
in Map.insertWith (Map.unionWith (<>)) shnm cinf m
return (nrParticipants, groupsPossible, infoMap, correctorMap)
let -- create aggregate maps
sheetMap :: Map SheetName CorrectionInfo
sheetMap = Map.map fold infoMap
corrMap :: Map (Maybe UserId) CorrectionInfo
corrMap = Map.unionsWith (<>) $ Map.elems infoMap
sheetNames = Map.keys infoMap
let -- whamlet convenience functions
showCorrector :: Maybe UserId -> Widget
showCorrector (Just uid)
| Just (_,User{..}) <- Map.lookup uid correctorMap
= nameEmailWidget userEmail userDisplayName userSurname
showCorrector _ = [whamlet|_{MsgNoCorrectorAssigned}|]
showDiffDays :: Maybe NominalDiffTime -> Text
showDiffDays = foldMap formatDiffDays
showAvgsDays :: Maybe NominalDiffTime -> Integer -> Text
showAvgsDays Nothing _ = mempty
showAvgsDays (Just dt) n = formatDiffDays $ dt / fromIntegral n
let headingShort = MsgMenuCorrectionsAssign
headingLong = prependCourseTitle tid ssh csh MsgMenuCorrectionsAssign
siteLayoutMsg headingShort $ do
setTitleI headingLong
-- TODO: Move whamlet into separate Widget-File, once completed
[whamlet|
<div>
<h2>_{MsgCorrectionSheets}
_{MsgCourseParticipants nrParticipants}
<table .table .table--striped .table--hover>
<tr .table__row .table__row--head>
<th .table__th>_{MsgSheet}
$if groupsPossible
<th .table__th>_{MsgNrSubmittorsTotal}
<th .table__th>_{MsgNrSubmissionsTotal}
<th .table__th>_{MsgNrSubmissionsNotAssigned}
<th .table__th>_{MsgNrSubmissionsNotCorrected}
<th .table__th colspan=3>_{MsgCorrectionTime}
$forall (sheetName, CorrectionInfo{ciSubmittors, ciSubmissions, ciAssigned, ciCorrected, ciMin, ciTot, ciMax}) <- Map.toList sheetMap
<tr .table__row>
<td .table__td>^{simpleLink (toWidget sheetName) (CSheetR tid ssh csh sheetName SSubsR)}
$if groupsPossible
<td .table__td>#{ciSubmittors}
<td .table__td>#{ciSubmissions}
<td .table__td>#{ciSubmissions - ciAssigned}
<td .table__td>#{ciSubmissions - ciCorrected}
<td .table__td>#{showDiffDays ciMin}
<td .table__td>#{showAvgsDays ciTot ciCorrected}
<td .table__td>#{showDiffDays ciMax}
<div>
<h2>_{MsgCorrectionCorrectors}
<table .table .table--striped .table--hover>
<tr .table__row .table__row--head>
<th .table__th>_{MsgCorrector}
<th .table__th>_{MsgNrSubmissionsTotal}
<th .table__th>_{MsgNrSubmissionsNotCorrected}
<th .table__th colspan=3>_{MsgCorrectionTime}
$forall shn <- sheetNames
<th .table__th colspan=3>#{shn}
$forall (CorrectionInfo{ciCorrector, ciSubmissions, ciCorrected, ciMin, ciTot, ciMax}) <- Map.elems corrMap
<tr .table__row>
<td .table__td>^{showCorrector ciCorrector}
<td .table__td>#{ciSubmissions}
<td .table__td>#{ciSubmissions - ciCorrected}
<td .table__td>#{showDiffDays ciMin}
<td .table__td>#{showAvgsDays ciTot ciCorrected}
<td .table__td>#{showDiffDays ciMax}
$forall shn <- sheetNames
$maybe smap <- Map.lookup shn infoMap
$maybe CorrectionInfo{ciAssigned,ciCorrected,ciTot} <- Map.lookup ciCorrector smap
<td .table__td>#{ciAssigned}
<td .table__td>#{ciAssigned - ciCorrected}
<td .table__td>#{showAvgsDays ciTot ciCorrected}
$nothing
<td .table__td colspan=3>
$nothing
<td .table__td colspan=3>
|]

View File

@ -136,7 +136,7 @@ postTDeleteR tid ssh csh tutn = do
return E.countRows return E.countRows
return (course, tutorial, participants :: E.SqlExpr (E.Value Int)) return (course, tutorial, participants :: E.SqlExpr (E.Value Int))
, drRenderRecord = \(Entity _ Course{..}, Entity _ Tutorial{..}, E.Value ps) -> , drRenderRecord = \(Entity _ Course{..}, Entity _ Tutorial{..}, E.Value ps) ->
return [whamlet|_{prependCourseTitle courseTerm courseSchool courseShorthand (CI.original tutorialName)} (#{tshow ps} _{MsgParticipantsN ps})|] return [whamlet|_{prependCourseTitle courseTerm courseSchool courseShorthand (CI.original tutorialName)} (_{MsgParticipantsN ps})|]
, drRecordConfirmString = \(Entity _ Course{..}, Entity _ Tutorial{..}, E.Value ps) -> , drRecordConfirmString = \(Entity _ Course{..}, Entity _ Tutorial{..}, E.Value ps) ->
return [st|#{termToText (unTermKey courseTerm)}/#{unSchoolKey courseSchool}/#{courseShorthand}/#{tutorialName}+#{tshow ps}|] return [st|#{termToText (unTermKey courseTerm)}/#{unSchoolKey courseSchool}/#{courseShorthand}/#{tutorialName}+#{tshow ps}|]
, drCaption = SomeMessage MsgTutorialDeleteQuestion , drCaption = SomeMessage MsgTutorialDeleteQuestion
@ -199,7 +199,7 @@ postTCommR tid ssh csh tutn = do
[E.Value isTutorialUser] <- E.select . return . E.exists . E.from $ \tutorialUser -> [E.Value isTutorialUser] <- E.select . return . E.exists . E.from $ \tutorialUser ->
E.where_ $ tutorialUser E.^. TutorialParticipantUser E.==. E.val uid E.where_ $ tutorialUser E.^. TutorialParticipantUser E.==. E.val uid
E.&&. tutorialUser E.^. TutorialParticipantTutorial E.==. E.val tutid E.&&. tutorialUser E.^. TutorialParticipantTutorial E.==. E.val tutid
isAssociatedCorrector <- evalAccessForDB (Just uid) (CourseR tid ssh csh CNotesR) False isAssociatedCorrector <- evalAccessForDB (Just uid) (CourseR tid ssh csh CNotesR) False
isAssociatedTutor <- evalAccessForDB (Just uid) (CourseR tid ssh csh CTutorialListR) False isAssociatedTutor <- evalAccessForDB (Just uid) (CourseR tid ssh csh CTutorialListR) False

View File

@ -0,0 +1,45 @@
module Handler.Utils.Corrections where
import Import
-- CorrectionInfo has seeming redundancies, but these are useful for aggregation
-- INVARIANT: isJust ciTot `implies` ciCorrected > 0
data CorrectionInfo = CorrectionInfo
{ ciSubmittors, ciSubmissions, ciAssigned, ciCorrected :: Integer
, ciCorrector :: Maybe UserId
, ciTot, ciMin, ciMax :: Maybe NominalDiffTime
}
instance Semigroup CorrectionInfo where
corrA <> corrB =
assert (isJust (ciTot corrA) `implies` (ciCorrected corrA > 0)) $
assert (isJust (ciTot corrB) `implies` (ciCorrected corrB > 0))
CorrectionInfo
{ ciSubmittors = ciSubmittors `mergeWith` (+)
, ciSubmissions = ciSubmissions `mergeWith` (+)
, ciAssigned = ciAssigned `mergeWith` (+)
, ciCorrected = ciCorrected `mergeWith` (+)
, ciCorrector = ciCorrector `mergeWith` keepEqual
, ciTot = ciTot `mergeWith` ignoreNothing (+)
, ciMin = ciMin `mergeWith` ignoreNothing min
, ciMax = ciMax `mergeWith` ignoreNothing max
}
where
mergeWith :: (CorrectionInfo -> a) -> (a -> a -> c) -> c
mergeWith prj f = on f prj corrA corrB
keepEqual (Just x) (Just y) | x==y = Just x
keepEqual _ _ = Nothing
instance Monoid CorrectionInfo where
mappend = (<>)
mempty = CorrectionInfo { ciSubmittors = 0
, ciSubmissions = 0
, ciAssigned = 0
, ciCorrected = 0
, ciCorrector = Nothing
, ciMin = Nothing
, ciTot = Nothing
, ciMax = Nothing
}

View File

@ -2,6 +2,7 @@ module Handler.Utils.DateTime
( utcToLocalTime ( utcToLocalTime
, localTimeToUTC, TZ.LocalToUTCResult(..) , localTimeToUTC, TZ.LocalToUTCResult(..)
, toMidnight, beforeMidnight, toMidday, toMorning , toMidnight, beforeMidnight, toMidday, toMorning
, formatDiffDays
, formatTime, formatTime', formatTimeW , formatTime, formatTime', formatTimeW
, getTimeLocale, getDateTimeFormat , getTimeLocale, getDateTimeFormat
, validDateTimeFormats, dateTimeFormatOptions , validDateTimeFormats, dateTimeFormatOptions
@ -29,6 +30,37 @@ import qualified Data.Set as Set
import Data.Time.Clock.System (systemEpochDay) import Data.Time.Clock.System (systemEpochDay)
--------------------
-- NominalDiffTime
-- | One hour in 'NominalDiffTime'.
nominalHour :: NominalDiffTime
nominalHour = 3600
-- | One minute in 'NominalDiffTime'.
nominalMinute :: NominalDiffTime
nominalMinute= 60
formatDiffDays :: NominalDiffTime -> Text
formatDiffDays t
| t > nominalDay = inDays <> "d"
| t > nominalHour = inHours <> "h"
| t > nominalMinute = inMinutes <> "m"
| otherwise = tshow $ roundToDigits 0 t
where
convertBy :: NominalDiffTime -> Double
convertBy len = realToFrac $ roundToDigits 1 $ t / len
inDays = tshow $ convertBy nominalDay
inHours = tshow $ convertBy nominalHour
inMinutes = tshow $ convertBy nominalMinute
------------
-- UTCTime
utcToLocalTime :: UTCTime -> LocalTime utcToLocalTime :: UTCTime -> LocalTime
utcToLocalTime = TZ.utcToLocalTimeTZ appTZ utcToLocalTime = TZ.utcToLocalTimeTZ appTZ
@ -52,7 +84,6 @@ toMorning :: Day -> UTCTime
toMorning d = localTimeToUTCTZ appTZ $ LocalTime d $ TimeOfDay 6 0 0 toMorning d = localTimeToUTCTZ appTZ $ LocalTime d $ TimeOfDay 6 0 0
class FormatTime t => HasLocalTime t where class FormatTime t => HasLocalTime t where
toLocalTime :: t -> LocalTime toLocalTime :: t -> LocalTime

View File

@ -343,6 +343,17 @@ notUsedT = notUsed
----------
-- Bool --
----------
-- | Logical implication, readable synonym for (<=) which appears the wrong way around
implies :: Bool -> Bool -> Bool
implies True x = x
implies _ _ = True
------------- -------------
-- Numeric -- -- Numeric --
------------- -------------
@ -523,12 +534,6 @@ flipMaybe :: b -> Maybe a -> Maybe b
flipMaybe x Nothing = Just x flipMaybe x Nothing = Just x
flipMaybe _ (Just _) = Nothing flipMaybe _ (Just _) = Nothing
maybeAdd :: Num a => Maybe a -> Maybe a -> Maybe a -- treats Nothing as neutral/zero, unlike fmap/ap
maybeAdd (Just x) (Just y) = Just (x + y)
maybeAdd Nothing y = y
maybeAdd x Nothing = x
-- | Deep alternative to avoid any occurrence of Nothing at all costs, left-biased -- | Deep alternative to avoid any occurrence of Nothing at all costs, left-biased
deepAlt :: Maybe (Maybe a) -> Maybe (Maybe a) -> Maybe (Maybe a) deepAlt :: Maybe (Maybe a) -> Maybe (Maybe a) -> Maybe (Maybe a)
deepAlt Nothing altSnd = altSnd deepAlt Nothing altSnd = altSnd
@ -574,6 +579,12 @@ mcons :: Maybe a -> [a] -> [a]
mcons Nothing xs = xs mcons Nothing xs = xs
mcons (Just x) xs = x:xs mcons (Just x) xs = x:xs
-- | apply binary function to maybes, but ignores Nothing by using id if possible, unlike fmap/ap
ignoreNothing :: (a -> a -> a) -> Maybe a -> Maybe a -> Maybe a
ignoreNothing _ Nothing y = y
ignoreNothing _ x Nothing = x
ignoreNothing f (Just x) (Just y) = Just $ f x y
newtype NTop a = NTop { nBot :: a } -- treat Nothing as Top for Ord (Maybe a); default implementation treats Nothing as bottom newtype NTop a = NTop { nBot :: a } -- treat Nothing as Top for Ord (Maybe a); default implementation treats Nothing as bottom
instance Eq a => Eq (NTop (Maybe a)) where instance Eq a => Eq (NTop (Maybe a)) where