Sense is a VHLL (very high level lang.), FP (functional programming) language that builds a layer of abstraction above the industry standard general purpose programming languages (like Java, Kotlin, Kotlin, Swift, JS, Python) with the purpose to create software without caring about implementation details.
"Programming is a game of information and logic, not of libraries, frameworks and terminology."
— Iliyan Germanov, creator of SenseLang
Sense purpose is to create software fast and express domain (business) logic by providing only the 100% necessary information and not a single line above that. Meaning that when you're writing Sense you won't care about:
boilerplate codecomplex design patternslibraries & frameworksthreading
Programming in Sense should be about thinking and expressing your logic freely in a concise, type-safe and functional way.
Benefits
- Easy to learn: minimal and simple syntax.
- Focus only on what matters: write only what matters - domain logic and algorithms.
- Platform agnostic: Android, iOS, Web, Desktop, Backend
- Frameworks/libraries agnostic: you can target which libs/frameworks you want and the Sense Compiler will generate the code for you.
Tradeoffs
- Performance: such high-level of abstraction certainly comes with some efficiency costs.
⚠️ Experimental: WIP to be developed.- No adoption & community: if Sense makes sense maybe one day millions of devs would use it.
Sense Lang: correctness, simplicity, and convenience over efficiency, low-level control and implementation details.
"Sense" name meaning:
- "sense" like a purpose.
- "sense" like a feeling.
- "sense" like make sense.
Every program is essentially a domain data representation of a problem fed into a chain of functions that transforms input A->B->C->N
and produces side-effects along the way.
Sense is designed to easily model any domain into data
, express your domain logic into functions with cases f :: A -> B | C
, handle side-effects (IO) and $(state)
gracefully.
If you're not familiar with Haskell and FP, Sense's syntax will not make sense to you at first. So take a deep breath and let's begin with an example.
readUserAge :: Int
isAdult :: Int -> Bool
age> age > 18
data WithAlcohol
data NoAlcohol
cocktailType :: Bool -> WithAlcohol | NoAlcohol
adult > if(adult)
True> WithAlcohol
False> NoAlcohol
serveCocktail :: WithAlcohol | NoAlcohol -> String
WithAlcohol> "Adults cocktail."
NoAlcohol> "Kids cocktail."
@Main
cocktailMixer :: Unit
|> readUserAge |> isAdult |> cocktailType
|> serveCocktail |> print
Meaning that x
can be of either type A
, B
or C
graph LR;
x("x :: A | B | C")
A(A)
B(B)
C(C)
f("f :: A | B | C -> String")
x -- "Case 1" --> A
x -- "Case 2" --> B
x -- "Case 3" --> C
A --> f
B --> f
C --> f
f :: A | B | C -> String
a:A> "Case A: " + a
B> "Case B"
C> "Case C"
Executes {code}
with an argument a
if it matches type A
. Used to match |
OR types - something like a weird if
.
Case Syntax: argName(optional) : ArgType (optional)> {code}
sayNumber :: Int -> String
x> if(x > 10)
True> if(x == 13)
True> log("The number is 13!")
> "It's 13."
False> "It's greater than 10 and not 13."
False> "It's less than 10."
functionName :: A, B, C -> T
functionName
is a function that accepts A
, B
and C
types and returns T
as output.
version :: String
version
is a function that accepts Unit
(nothing as input) and returns String
Unit = nothing
Our first Sense function:
helloWorld :: Unit
> print("Hello, world")
And a more realistic example:
userStatus :: User | Admin -> String
User> "Basic user"
adm: Admin> let level = adm.accessLevel
> if(adm.isActive)
True> "Active admin, level " + level
False> "Inactive admin, level " + level
The last line of case in a function always returns a value.
To invoke a function simply f(arg1, arg2, ... , argN)
.
f :: A -> B
g :: B -> C
h :: C -> D
comp :: A -> C
|> f |> g |> h
// equivalent to a> f(a) |> g |> h
The |>
operator feeds a value from the left side to a function on the rigth side.
- Declare:
let varName: VarType = value
. - Read, just:
varName
- Declare:
state x: X = initalValue
. - Read, just:
x
- Update:
x = newValue
.
let step = 10
state counter = 0
$(step, counter) // gives access to "step" and "counter"
stepUp :: Int
> counter += step
> counter
Int
: integer (long size).Decimal
: double precision.String
: just a string.Unit
: nothing.
Optional value of type T
. You can case-match via:
doubleOrZero :: Int? -> Int
x> x * 2 // non-null optional
null> 0 // optional is null
Self-explanatory will document later.
Self-explanatory will document later.
data BasicRole
data AdminRole
data User(
firstName: String
lastName: String?
role: BasicRole | AdminRole
friends: [User]
monthYear: (Int, Int)
height: Decimal
)
All data fields in Sense are immutable.
Everything else like Bool
, for
, Map
, Stack
, Graph
, map
, filter
, DateTime
, etc is included with the standard library.
At the core of Sense is decision-makig, the "case-matching": what to do when case X
happen and what can happen when you do Y
operation.
To minimize confusion, we'll assume that every program can be represented by a graph of functions (decisions) and analyze a concrete example.
Login/Register email example
graph LR;
emailInput(inputEmail)
validateEmail(validateEmail)
sendCheckEmailReq(sendCheckEmailReq)
login(login)
register(register)
inputPass(inputPass)
validatePass(validatePass)
sendLoginReq(sendLoginReq)
sendRegisterReq(sendRegisterReq)
emailInput -- "email: String" --> validateEmail
validateEmail -- "Valid(email)" --> sendCheckEmailReq
validateEmail -- "Invalid" --> emailInput
sendCheckEmailReq -- "Taken(email)" --> login
sendCheckEmailReq -- "Available(email)" --> register
sendCheckEmailReq -- "HTTPErr" --> emailInput
login -- "LoginPass(email)" --> inputPass
register -- "RegPass(email)" --> inputPass
inputPass -- "pass: String, LoginPass | RegPass" --> validatePass
validatePass -- "Invalid, LoginPass | RegPass" --> inputPass
validatePass -- "RegisterReq(email, pass)" --> sendRegisterReq
validatePass -- "LoginReq(email, pass)" --> sendLoginReq
It's a common use-case to do validation, send HTTP requests and based on their response execute different logic. Here's how this can be implemented in Sense.
SmartAuthenticate.sense
inputEmail :: String
> inputField("email")
data ValidEmail(
email: String
)
data Invalid
validateEmail :: String -> ValidEmail | Invalid
// pseudo validation
email> if(contains('@', email) && length(email) > 3)
True> ValidEmail(email)
False> Invalid
data Taken(
email: String
)
data Available(
email: String
)
sendCheckEmailReq :: ValidEmail -> Taken | Available | HttpErr
valid> send("/checkEmail", valid)
response: Ok> if(response.body == "taken")
True> Taken(email)
False> Available(email)
// HttpErr> case can be skipped
data LoginUI
data RegisterUI
data None
data UIState(
state: LoginUI | RegisterUI | None
)
state uiState = UIState(None)
data LoginPass(
email: String
)
data RegPass(
email: String
)
$(uiState)
login :: Taken -> LoginPass
taken> uiState = LoginUI // modify state
> LoginPass(taken.email)
$(uiState)
register :: Available -> RegPass
avail> uiState = RegisterUI
> RegPass(avail.email)
inputPass :: LoginPass | RegPass -> (String, LoginPass | RegPass)
login: LoginPass> (inputField("login_pass"), login)
reg: RegPass> (inputField("reg_pass"), reg)
data RegisterReq(
email: String
pass: String
)
data LoginReq(
email: String
pass: String
)
validatePass :: String, LoginPass | RegPass -> LoginReq | RegisterReq | Invalid
pass, type> if(length(pass) > 6)
True> type // valid password
LoginPass> LoginReq(email=type.email, pass=pass)
ReqPass> RegisterReq(email=type.email, pass=pass)
False> Invalid
@Main
program :: LoginReq | RegisterReq
|> inputEmail |> validateEmail
Valid|> sendCheckEmailReq
Taken|> login |> passFlow
Available|> register |> passFlow
HttpError> program() // restart
Invalid> program() // restart
passFlow :: LoginPass | RegPass -> LoginReq | RegisterReq
|> inputPass |> validatePass
Invalid> passFlow() // repeat flow
Will host simple Sense playground when the Sense Compiler is ready.
Does SenseLang make sense to you? Help us make it better:
- give feedback
- contribute
- join the team
- help us find funding