LanguagesPaper

advertisement
Jared Wheeler
COP6557
Languages Research Paper
Programming languages are diverse in variety and implementation. Each has unique points and
some commonalities. Polog is a programming language which has survived the test of time, yet has
never become mainstream due to its specificity to its problem solving area. Clojure is a modern
adaptation of Lisp-1 to run on JVM and other more object-oriented virtual machines. F# is Microsoft’s
foray into functional programming for their .NET framework. Each language has interesting
implementation to solve certain problems.
Prolog, “Programming in Logic”, is a declarative language which solves problems for the
programmer. Developed in 1972 by Alain Colmerauer and Phillipe Roussel, Prolog was one of the first
successful logic programming languages. Prolog is based upon facts, rules, and queries. Facts are basic
assertions, rules are inferences relating facts, and queries are questions; the first two are stored in a
knowledge base file and the queries are input by the user. Facts are identified as a predicate with its
two targets in parenthesis; this grows into a graph-like structure of relationships between targets. Rules
and queries determine datatypes by naming constraints: lowercase words are constants, those that start
with uppercase or underscore are variables that Prolog will try to determine at runtime. Lists are
indicated by square brackets ([]) and are of variable length, while tuples indicated by parenthesis have
variable length. Lists and Tuples can be divided with pipe (|) delimiters, to where the first element is on
the left, the rest on the right, frequently referred to as [Head|Tail]. Recursion is possible within the
Rules; however it can very easily cause a memory overflow. The use of Tail Recursion Optimization,
placing the recursive call at the end of the Rule, will cause Prolog to discard the previous stack when it
enters the nesting, and therefore avoids the overflow. Unlike other languages, Prolog has no
assignments; rather it unifies the variable with possible constants seeking two that match. Unification is
the key to how Prolog solves queries; it performs depth-first traversal of its constructed graph of rules,
utilizing the inferences to guide it towards the answer for the query. In the end, it will relay the
successful unifications to the user as an answer to each variable in the query. All of this relays Prolog as
a simply written, yet powerful, language for problem solving.
Perhaps the most confusing part of the language is the output, which will be the discovered
answers, followed by a yes or no. This yes or no answer is to the unspoken question of if execution
completed. The program responds yes if execution of its search across the knowledgebase completed.
It answers no if the execution could not be completed, and therefore the answers provided may be
complete or incomplete. On top of this lack of commitment to complete answers, Prolog has a steep
learning curve, especially for developers. Developers are used to designing an algorithm to solve a
problem; Prolog is the algorithm for solving a problem, it simply requires a complete mapping of the
problem space and a query that properly relays the desired question across the knowledge base. Prolog
is implemented to run in many environments, from GNU to JVM or CLR, although these different
implementations may have additional features or quirks that general Prolog does not have. Prolog is
proven to be powerful for solving problems in systems with constraints, such as puzzles, AI, and natural
language processing; however it has problems scaling to very large data sets, has a steep learning curve,
and cannot be used outside of its specialty area.
Clojure is the language that wants to bring Lisp into modern times. It merges the ideals of Lisp
with the power of modern OOP, powered by JVM, but also adaptable for CLR, JavaScript, and Python.
Clojure is a dynamically typed functional language with the methodology of “data as code”. Clojure, like
Lisp, utilizes a prefix notation and a large number of parentheses, for all operations within the language.
Clojure utilizes the underlying JVM datatypes, and then has four primary data structures used in it.
These four data types are List, Vector, Set, and Map. A list is an ordered collection of elements
surrounded by parenthesis; lists are used idiomatically to hold code as they are always evaluated as
functions. Vectors are an ordered collection of elements optimized for random access, surrounded by
square brackets; these are used idiomatically to hold data. Maps are key-value pair listings surrounded
by braces; Clojure allows commas to delimit the pairs. Also, maps can utilize the keyword as a function
to find the value. Sets are indicated by a pound, and then surrounded by braces; they contain
unordered elements. Sets can be used as a function to determine if a searched for element is within it.
Functions are defined by the defn function with a name and a vector of parameters. Clojure allows
interfaces, created by defprotocol and instantiated with defrecord. The datatypes in Clojure are
immutable, and to change values, will be recreated. Clojure allows the use of let to temporarily bind a
value to a variable. Clojure also allows the use of anonymous functions through use of pound
parenthesis around the body of the function. All collections, strings, file systems, and most Java
containers used in Clojure implement Sequences, which give a wide variety of preexisting functions,
such as every?, some?, filter, and for to make for loop-like structures. Also, Clojure features lazy
evaluation of ranges so that it is possible to do actions on infinite sets. Finally, as a functional language,
Clojure gives better concurrency safeguards, specifically the utilization of Software Transactional
Memory (STM). STM makes it so that references, data defined by a ref to data, can only be changed
within a transaction scope. The transaction scope operates like that of a database to stop conflicting
data manipulation. Clojure also allows atoms to be built: blocks of data which cannot be modified, but
can be overwritten.
However, Clojure is a complex language. Macros were extremely dangerous in Lisp, and while
some macros were not implemented within Clojure, they are still a danger for inexperienced users.
Macros allow users to utilize the “data as code” philosophy to create their own keywords. The prefix
notation takes effort in a programming world dominated by infix notation. Also, Clojure, despite being a
functional language, lacks tail recursion optimization due to the JVM unable to directly support it.
Therefore, in order to optimize recursive calls, the developer must put a loop at the start, and then the
keyword recur with the parameters being passed to indicate that recursion is being used. This method
of recursion is far less readable than other functional languages, especially with the lack of readability
that Clojure already presents with the sheer number of parenthesis. Clojure is flexible and powerful;
however this comes with costs of readability and complexity.
F# is functional language built by Microsoft for its CLR. F# is similar to Clojure in its language
goals of flexibility and power; however it does not abide by the same “data as code” mentality. F#
actually has fewer parentheses than standard programming languages. Functions are still called in
prefix notation; however, rather than the parenthesis of Clojure and Lisp, F# is white-space sensitive.
Variables are bound to values in let statements; once bound the value is immutable. It is possible to add
an attribute, “modifiable”, to the binding statement, therefore allowing a new value to be designated
for that variable. F# is a statically typed language; however adopts the look of a dynamic through type
inferencing, allowing the same powerful type-checking of the rest of the .NET Framework. It is possible
to specify the variable type through a (variableName:type) syntax; however this should only be
necessary during some function calls. Functions are a datatype in F#, and can therefore be passed to
other functions. Anonymous functions are built by lambda expressions, where a signature indicates its
function body with “->”. The three primary data structures are lists, arrays, and records. Lists are
denoted by square brackets containing a semi-colon delimited list; or they can be created as a range
with a “..” separating the start and exclusive stop numbers. List.map and List.filter allow functions to be
run across lists, with the list to be affected named after the function and its argument. Arrays are
enclosed with square bracket and pipe ([| |]). Records are grouped data with each member having a
name; these are defined as types in the language and are enclosed with brackets. Records are also
immutable; however can be rebuilt with altered values through {name with member=newValue} syntax.
Inference when instantiating a custom type can be sometimes wrong; however by invoking the member
from the parent type, it tells F# what type the entire object is. Attributes can also be optional, specified
as having “None” or “Some “+value. Discriminating joins allow for enumerations to be used. F# also
features the forward pipe operator (|>), which cuts down on function nesting by giving the functions to
be called in the order they are to be called.
The power, and complexities, of F# comes from the following traits of the language. Currying is
that despite multiple parameters in a function, each function is treated as having one parameter which
returns a function with another parameter, and so on until all of the parameters are used. This allows
more modular work when modifying the language through use of newly defined operators. However,
defining new operators has a danger of either overloading previous operators or completely overwriting
them. Overloads of operators can be specified within the local scope of a type. Active patterns and
Quotations build powerful sub-language abilities. Active patterns allow custom pattern matching within
F#. Quotations, designated by “<@ … @>”, can hold code fragments which will be type checked, but not
executed. Together they allow F# to be a powerful language for cross-compilation. F# 3.0 allows for
execution of the quotations by the CLR if desired. F# has very powerful and complex abilities, allowing
for great power to the user if they learn the idiosyncrasies of the language.
Prolog is the oldest of these languages, and has been adapted to many different runtime
environments. Clojure is the adaptation of Lisp for the JVM, and F# is a language for the Microsoft CLR.
Prolog was built to solve constrained problems, which it does with relative simple syntax, yet a complex
mindset. Clojure sought to make Lisp more modern and fix some of its shortcomings. F# is a functional
language of experimentation for Microsoft. All three succeed at their goals, although their price is
complexity and the learning curve.
Download