Postgres uses snake_case
. Tables should be plural (e.g. teachers
,
students
). Text-based enums are currently also snake_case
(except domain
abbreviations and math system which are ALLCAPS
), though this makes
interoperating with Haskell and JavaScript non-obvious.
If a table has a foreign key to another table, remove the product and subject prefix from the foreign key column's name unless doing so would introduce ambiguity.
Don't do this:
CREATE TABLE ela_adaptive_skill_practice_paragraphs (
id integer PRIMARY KEY NOT NULL,
ela_adaptive_skill_practice_content_id uuid
REFERENCES ela_adaptive_skill_practice_content NOT NULL,
type text NOT NULL,
content text NOT NULL,
caption text,
position integer NOT NULL,
UNIQUE (ela_adaptive_skill_practice_content_id, position),
CONSTRAINT valid_ela_adaptive_skill_practice_paragraph_position
CHECK (position >= 0)
);
Do this:
CREATE TABLE ela_adaptive_skill_practice_paragraphs (
id integer PRIMARY KEY NOT NULL,
- ela_adaptive_skill_practice_content_id uuid
- REFERENCES ela_adaptive_skill_practice_content NOT NULL,
+ content_id uuid REFERENCES ela_adaptive_skill_practice_content NOT NULL,
type text NOT NULL,
content text NOT NULL,
caption text,
position integer NOT NULL,
UNIQUE (ela_adaptive_skill_practice_content_id, position),
CONSTRAINT valid_ela_adaptive_skill_practice_paragraph_position
CHECK (position >= 0)
);
We only need to convey that this field is a pointer to "content". Any other information we could encode can be understood from context. Furthermore, the elided information is more likely to change. In fact, we no longer refer to this product as "AdaptiveSkillPractice". It's just "SkillsPractice" now. However, because we've encoded superfluous information in the column, the Haskell and JSON representations do too, which makes changing it a multi-step process.
Haskell uses camelCase
for identifiers and TitleCase
for types and
constructors. The latter is enforced by the language. Names that are being
ignored should either use the wildcard _
or specify what is being ignored
(e.g. _teacher
, _standard
).
Prefer short, single-letter names for polymorphic type variables and arguments. You'll notice a number of conventions with type variables.
Unconstrained type variables often start with a
and continue alphabetically
id :: a -> a
const :: a -> b -> a
Polymorphic lists are often "pluralized" by appending s
filter :: (a -> Bool) -> [a] -> [a]
filter _ [] = []
filter predicate (a:as)
| predicate a = a : bs
| otherwise = bs
where
bs = filter predicate as
f
is for Functor
and sometimes Applicative
and Alternative
fmap :: Functor f => (a -> b) -> f a -> f b
liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
guard :: Alternative f => Bool -> f ()
t
might be Traversable
or Foldable
foldr :: Foldable t => (a -> b -> b) -> b -> t a -> b
traverse :: (Applicative f, Traversable t) => (a -> f b) -> t a -> f (t b)
m
is almost always Monad
or something monad-transformer related
when :: Monad m => Bool -> m () -> m ()
liftIO :: MonadIO m => IO a -> m a
s
is for state and e
is for environment
put :: MonadState s m => s -> m ()
ask :: MonadReader e m => m e
These are conventions only, but picking something unusual for a common typeclass might surprise other readers.
Avoid using single-letter names or abbreviations for values with a concrete type, especially if that type is domain-specific. To crib from the Swift API Design Guidelines, "Clarity is more important than brevity."
For example, don't do this:
fetchCourseMemberships :: MonadIO m => Entity Teacher -> SqlReadT m [Entity CourseMembership]
fetchCourseMemberships (Entity tId teacher) =
select $ from $ \(ts `InnerJoin` cs `InnerJoin` cms) -> do
on $ cs ^. CourseId ==. cms ^. CourseMembershipCourseId
on $ cs ^. CourseTeacherId ==. ts ^. TeacherId
where_ $
case teacherSchoolId teacher of
Just sId ->
ts ^. TeacherSchoolId ==. val (Just sId)
Nothing ->
ts ^. TeacherId ==. val tId
pure cms
Do this instead:
fetchCourseMemberships :: MonadIO m => Entity Teacher -> SqlReadT m [Entity CourseMembership]
fetchCourseMemberships (Entity teacherId teacher) =
select $ from $ \(teachers `InnerJoin` courses `InnerJoin` memberships) -> do
on $ courses ^. CourseId ==. memberships ^. CourseMembershipCourseId
on $ courses ^. CourseTeacherId ==. teachers ^. TeacherId
where_ $
case teacherSchoolId teacher of
Just schoolId ->
teachers ^. TeacherSchoolId ==. val (Just schoolId)
Nothing ->
teachers ^. TeacherId ==. val teacherId
pure memberships
We tolerate a few abbreviations as components of identifiers (e.g. the num
portion of numQuestionsAnswered
), but there doesn't seem to be any broad
consensus about this.
Database access functions should follow standard prefixes and suffixes for the following cases:
fetch{...}
for a normalSELECT
fetch{...}Exists(By{...})
for aSELECT EXISTS
count{...}
for aCOUNT
And should follow a pattern that indicates:
- What it returns, specifically if it returns a whole resource or just one field
- What its arguments are, specifically if they are whole resources or fields
- A correct pluralization of arguments and returned values
{fetch|count}{Resource(Field)}(Exists)(By{Resource(Field)}(And{Resource(Field)}...))
This is to avoid duplication by aiding code search through "obvious" names: if
you know you need a Foo
by its barBat
, you should look for
fetchFooByBarBat
before writing it.
-- Good
fetchTeachersBySchoolId :: SchoolId -> SqlPersistT m [Entity Teacher]
fetchTeacherIdsBySchool :: School -> SqlPersistT m [TeacherId]
-- NOTE: We don't go as far as distinguishing Entity or not in this naming, so
-- these are also Good, with Resource ~ Teacher and Resource ~ Entity School
fetchTeachersBySchoolId :: SchoolId -> SqlPersistT m [Teacher]
fetchTeacherIdsBySchool :: Entity School -> SqlPersistT m [TeacherId]
fetchStudentEmailsByFirstNameAndLastName
:: NameComponent -- ^ First name
-> NameComponent -- ^ Last name
-> SqlPersistT m [EmailAddress]
fetchStudentEmailByFirstNameAndLastName
:: NameComponent -- ^ First name
-> NameComponent -- ^ Last name
-> SqlPersistT m (Maybe EmailAddress)
fetchTeacherExistsBySchoolId :: SchoolId -> SqlPersistT m Bool
countSessionsByAssignmentId
:: forall assignment session
. Session assignment ~ session
=> Key assignment
-> SqlPersistT m Natural
-- Bad
getAnswers :: SqlReadT [Answer]
fetchTeachersBySchool :: SchoolId -> SqlPersistT m [TeacherId]
fetchTeachersCountForStudent :: Student -> SqlPersistT m Int64
Use mkPersist
from persistent
to generate record
types corresponding to database tables. sqlSettings
will
autoprefix each field with the record's name. Note that the field names given in
the persistLowerCase
quasiquotation should exactly match the corresponding
column name in the database table except that the former is camelCase
and the
latter is snake_case
(see Postgres above). For example:
share [mkPersist sqlSettings, mkMigrate "migration"] [persistLowerCase|
User sql=users
name Text
age Natural
deriving Eq Show Ord Generic
|]
will generate a data declaration that looks like this:
data User = User
{ userName :: Text
, userAge :: Natural
}
deriving (Eq, Show, Ord, Generic)
Entities that are meant to appear in API requests or responses should have JSON instances that strip the prefixes:
instance ToJSON User where
toEncoding = genericToEncoding (unPrefix "user")
toJSON = genericToJSON (unPrefix "user")
instance FromJSON User where
parseJSON = genericParseJSON (unPrefix "user")
This will produce the following JSON:
{ "name": "Joe"
, "age": 29
}
For complicated Handler
requests or responses, where we use a custom type
instead of a domain object, such types should be named {Resource}{Method}
where Resource
matches the route and Method
is Sentence-case.
-- Good
data SchoolGet
-- ...
instance ToJSON SchoolGet where
-- ...
getSchoolR :: SchoolId -> Handler Value
getSchoolR schoolId = do
school <- get404 schoolId
sendStatusJSON status200 SchoolGet {- ... -}
data SchoolsPost
-- ...
instance FromJSON SchoolsPost where
-- ...
postSchoolsR :: Handler Value
postSchoolsR = do
SchoolsPost {..} <- requireJsonBody
-- ...
-- Bad
data GetSchool
-- Out of order (though gramatically attractive)
data SchoolGET
-- Wrong case
data SchoolResponse
-- Missing Method, redundant suffix
data GetSchools
-- Incorrect plurization
data CreateSchoolPost
-- Duplicate verb, incorrect pluralization
JavaScript uses camelCase
for identifiers and TitleCase
for classes and
React components. For flow types, we generally append a T
to the end of the
type, e.g. SkillT
. Names that are being ignored should be prefixed with an
underscore (e.g. _teacher
, _standard
). It's a common mistake (especially for
Haskellers) to use a lone _
and accidentally overwrite underscore in that
scope.
JSON keys should always be camelCase
. String enums that cross language
barriers should be snake_case
, since most enums that cross language barriers
are designed for postgres
first. Exceptions can be made for short initialisms
which may be ALLCAPS
(e.g. TEKS: MathSystemT
). Short initialisms that are
part of other identifiers should use snake_case
(e.g.
rti_coordinator: TeacherRoleT
).
Snake-case enum
data TeacherRole
= Teacher
| RtiCoordinator
| ...
-- Equivalent to
-- mkPersistEnumUsing (snakeCaseify . unCapitalize) ''TeacherRole
-- Produces
-- 'teacher'
-- 'rti_coordinator'
-- ...
mkPersistEnum ''TeacherRole
All-caps enum
data MathSystem
= CCSS -- Common Core Standard System
| TEKS -- Texas Essential Knowledge and Skills
| ...
-- Produces
-- 'CCSS'
-- 'TEKS'
mkPersistEnumUsing id ''MathSystem
Snake-case enum
data TeacherRole
= Teacher
| RtiCoordinator
| ...
-- Equivalent to
-- deriveJSONEnumUsing (snakeCaseify . unCapitalize) ''TeacherRole
-- Produces
-- "teacher"
-- "rti_coordinator"
-- ...
deriveJSONEnum ''TeacherRole
All-caps enum
data MathSystem
= CCSS -- Common Core Standard System
| TEKS -- Texas Essential Knowledge and Skills
| ...
-- Produces
-- "CCSS"
-- "TEKS"
-- ...
deriveJSONEnumUsing id ''MathSystem
JavaScript enums should be typed using flow
, e.g.:
type TeacherRoleT
= 'teacher'
| 'rti_coordinator'
| ...
One is encouraged to make smart constructors for enums to reduce the risk of
misspellings. flow
usually catches misspellings, but not always.
const TeacherRoles = {
Teacher: ('teacher': TeacherRoleT),
RtiCoordinator: ('rti_coordinator': TeacherRoleT),
...
}
-- e.g.
const role = TeacherRoles.Teacher