Values, variables and types
A value has a type; a local variable has a name and a current value. DirectSharp checks these relationships before the program runs. In an order application, a customer identifier, a quantity and an amount may all be numbers, but they serve different purposes. Choosing types deliberately makes those purposes easier to follow.
let / default: declare and initialize values
public class InvoiceMath
public static action Calculate(
quantity as Integer,
unitPrice as Decimal,
discountRate as Decimal
)
returns Decimal
require quantity > 0
require unitPrice >= 0
require discountRate >= 0 and discountRate <= 1
let subtotal = quantity * unitPrice
let discount = subtotal * discountRate
let total = subtotal - discount
return total
end action
end class
Every let supplies an initializer. The compiler infers the type of subtotal, discount and total from their operands. It does not leave a newly declared local waiting for an assignment on some later path.
Integer is useful for counts. Decimal is useful for decimal financial calculations. An integer parameter does not become a Decimal simply because a particular call happens to supply a small number. Ordinary type and conversion rules still apply.
Use an explicit type when inference would choose a different one or when the type communicates a policy:
public class ExchangeCalculation
public static action ConvertToLocal(amount as Decimal)
returns Decimal
let exchangeRate as Decimal = 61.5
return amount * exchangeRate
end action
end class
The name ConvertToLocal is application code, not a language conversion operation. The fixed rate keeps this example focused on types; real currency conversion needs a rate source, effective date and rounding policy.
readonly / assignment: control what can change
Locals are mutable unless declared readonly. Reassignment keeps the variable's type:
public class RunningTotal
public static action Sum(
amounts as Global.System.Collections.Generic.IEnumerable <of Decimal>
)
returns Decimal
require amounts is not nothing
let total as Decimal = 0
for_each amount in amounts
total += amount
end for_each
return total
end action
end class
The loop visits the supplied sequence without removing its elements. total += amount updates the running total. The iteration binding itself is readonly: reassigning amount is not how you replace an element in the source collection.
A readonly local prevents rebinding, rather than freezing everything reachable through it:
public class PendingCustomers
public static action Create()
let readonly names = new Global.System.Collections.Generic.List <of String>()
names.Add("Ana")
names.Add("Boris")
Global.System.Console.WriteLine(names.Count)
end action
end class
names keeps pointing to this List, while the List's own API can still add items. This distinction matters when sharing collections between components.
lazy: compute a value when first needed
Declare let names = lazy BuildNames(customers) when an index should be built only if the action actually needs it. BuildNames is an ordinary application action returning a dictionary. The first normal use of names evaluates the complete initializer; subsequent uses share its cached value. There is no source .Value access and no automatic background start.
The binding is assign-once. A mutable captured variable is read when initialization runs, and an initialization failure is cached rather than retried. Once-only initialization does not freeze the returned object. The optional report example shows the complete class and explains input/resource lifetimes.
nothing / ??: handle absent values
nothing means absence. default asks for the effective default of a known type. A String default is an empty string, while an ordinary class reference commonly defaults to absence.
public class ContactLabels
public static action Display(email as String)
returns String
if email is nothing then
return "No address supplied"
end if
if email is whitespace then
return "Address is blank"
end if
return email.Trim()
end action
end class
An absent address and a blank address can require different feedback. Calling Trim only after checking absence makes the receiver's availability explicit.
Use ?? when a fallback represents the application's intent:
public class DeliveryNotes
public static action Format(note as String)
returns String
let visibleNote = note ?? "No delivery instructions"
return "Delivery note: {{visibleNote}}"
end action
end class
The source expression is evaluated once, and the fallback is evaluated only if it is needed. Substituting a display message here does not mutate the caller's value.
Default means the type's effective default, which can itself be absence:
let customer = existingCustomer ?? default
For a Customer class with no visible custom default, a missing reference stays nothing. The expression is valid and preserves a present reference. It does not create a Customer. If the application needs an instance, supply an explicit new Customer construction or a valid custom default; ?? default follows the normal default-resolution rule.
Nullable value types use normal .NET Nullable <of T> types. DirectSharp arithmetic can substitute the effective default for a missing supported numeric operand without changing the stored value. When missing data has business meaning, express that meaning before calculating:
public class RecordedDiscounts
public static action Apply(
amount as Decimal,
discount as Global.System.Nullable <of Decimal>
)
returns Decimal
require amount >= 0
if discount is nothing then
return amount
end if
require discount.Value >= 0 and discount.Value <= amount
return amount - discount.Value
end action
end class
You can also let an absent initializer select a nullable local:
let recordedAmount as Decimal = nothing
recordedAmount = 125
recordedAmount = nothing
RecordedAmount is a real Nullable <of Decimal> throughout. A local initialized as nonnullable Decimal keeps that type and cannot later receive nothing. For parameters and results, write Nullable <of Decimal> explicitly when absence belongs in that contract. References such as String can already hold nothing.
An imported API receives the actual argument you pass. DirectSharp arithmetic does not turn every absent value into zero at a method call, in serialization or in a comparison.
Use ?? throw when absence must fail at a particular expression:
let customer = LoadCustomer(id) ?? throw
customer = nothing
The application action returns a Customer reference. The initializer checks its result immediately. That check adds no permanent rule to the local: the later assignment is allowed for this mutable reference type. If a later operation requires presence, check it there with require or another ?? throw expression. Ordinary type and readonly restrictions still apply.
let from / let_if: extract related values
Use let from to name several values from an object without repeating its source expression:
let from LoadCustomer(id)
name = .Name
email as String = .Email
end let
This fragment assumes an application LoadCustomer action returning a present Customer with Name and Email properties. The action is called once; both reads use that captured customer, with Name read before Email. Each extraction has an explicit local name, and an explicit type is optional. This is ordinary object access, not a copy or an ownership scope.
Let from requires a present source and fails before extraction if it is nothing. When a missing customer should produce absent values, choose conditional extraction:
let_if from LoadCustomer(id)
name = .Name
email as String = .Email
end let_if
Global.System.Console.WriteLine(name ?? "Customer was not found")
The application supplies String properties. A missing source assigns nothing to both locals; it does not substitute empty strings or evaluate either getter. A present source extracts normally. An exception thrown by LoadCustomer or an evaluated getter still propagates. The declarations remain available after end let_if.
Value-type results use native CLR nullable types automatically, including when their type is written explicitly. With Integer Age and Decimal CreditLimit properties:
let_if from LoadCustomer(id)
age as Integer = .Age
creditLimit = .CreditLimit
end let_if
if age is not nothing then
Global.System.Console.WriteLine(age.Value)
end if
Age is Nullable <of Integer> and creditLimit is Nullable <of Decimal>. Their types stay nullable when a customer is found; they do not change with the runtime branch. If the customer is missing, both hold nothing rather than zero. Use a presence check, or choose an explicit fallback with ??, before calling an API that requires a non-nullable value.
operators: arithmetic, comparison and assignment
= compares values in a condition and assigns a value in an assignment statement. <> means unequal. and and or combine conditions with short-circuit evaluation. Put checks in the order needed to make subsequent access safe.
Ordinary / produces fractional division for integer operands: 3 / 2 has the value 1.5. Use div when you want an integral whole-number quotient and mod when you want the remainder. 3.5 div 2 is a Long with value 1: divide first, then convert the quotient toward zero. Result conversion follows normal CLR checked/unchecked behavior. Numeric compatibility and overflow still depend on the operand types and the selected operation.
Assignment chains such as first = second = Calculate() support simple writable locals and parameters only. The call runs once and assignments proceed from right to left. Use separate statements for fields, properties and indexed targets.
For an ordinary exchange, use swap(first, second). Both locations must be writable and have the same effective type. This avoids inventing a multiple-assignment form such as first, second = second, first. The helper works with ordinary reference and value types, not only native atomic numeric types. Explicit atomic swap(first, second) requests protected exchange; swap directly in a concurrent body uses that protection automatically. Consistent reads of shared pairs also need compatible coordination; see concurrency.
String comparisons written with language operators ignore case by default using ordinal comparison. Imported .NET methods retain their own contracts. For a protocol or identifier that requires exact casing, select an explicit .NET comparison:
public class ProtocolNames
public static action IsExact(actual as String, expected as String)
returns Boolean
return Global.System.String.Equals(
actual,
expected,
Global.System.StringComparison.Ordinal
)
end action
end class
For the full type-default and arithmetic rules, see types and operators.
Practice: values, defaults and absence
Predict absence, defaults and readonly mutation before reading the answer.